mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
add inherited bindings
This commit is contained in:
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Support lazy-instantiated Screens (callables in App.SCREENS) https://github.com/Textualize/textual/pull/1185
|
||||
- Display of keys in footer has more sensible defaults https://github.com/Textualize/textual/pull/1213
|
||||
- Add App.get_key_display, allowing custom key_display App-wide https://github.com/Textualize/textual/pull/1213
|
||||
- Added "inherited bindings" -- BINDINGS classvar will be merged with base classes, unless inherit_bindings is set to False
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -458,7 +458,6 @@ class Compositor:
|
||||
|
||||
# Add top level (root) widget
|
||||
add_widget(root, size.region, size.region, ((0,),), layer_order, size.region)
|
||||
root.log(map)
|
||||
return map, widgets
|
||||
|
||||
@property
|
||||
|
||||
@@ -22,7 +22,7 @@ from rich.tree import Tree
|
||||
|
||||
from ._context import NoActiveAppError
|
||||
from ._node_list import NodeList
|
||||
from .binding import Bindings, BindingType
|
||||
from .binding import Binding, Bindings, BindingType
|
||||
from .color import BLACK, WHITE, Color
|
||||
from .css._error_tools import friendly_list
|
||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
@@ -97,9 +97,16 @@ class DOMNode(MessagePump):
|
||||
|
||||
# True if this node inherits the CSS from the base class.
|
||||
_inherit_css: ClassVar[bool] = True
|
||||
|
||||
# True to inherit bindings from base class
|
||||
_inherit_bindings: ClassVar[bool] = True
|
||||
|
||||
# List of names of base classes that inherit CSS
|
||||
_css_type_names: ClassVar[frozenset[str]] = frozenset()
|
||||
|
||||
# Generated list of bindings
|
||||
_merged_bindings: ClassVar[Bindings] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -127,7 +134,7 @@ class DOMNode(MessagePump):
|
||||
self._auto_refresh: float | None = None
|
||||
self._auto_refresh_timer: Timer | None = None
|
||||
self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)}
|
||||
self._bindings = Bindings(self.BINDINGS)
|
||||
self._bindings = self._merged_bindings or Bindings()
|
||||
self._has_hover_style: bool = False
|
||||
self._has_focus_within: bool = False
|
||||
|
||||
@@ -152,12 +159,16 @@ class DOMNode(MessagePump):
|
||||
"""Perform an automatic refresh (set with auto_refresh property)."""
|
||||
self.refresh()
|
||||
|
||||
def __init_subclass__(cls, inherit_css: bool = True) -> None:
|
||||
def __init_subclass__(
|
||||
cls, inherit_css: bool = True, inherit_bindings: bool = True
|
||||
) -> None:
|
||||
super().__init_subclass__()
|
||||
cls._inherit_css = inherit_css
|
||||
cls._inherit_bindings = inherit_bindings
|
||||
css_type_names: set[str] = set()
|
||||
for base in cls._css_bases(cls):
|
||||
css_type_names.add(base.__name__)
|
||||
cls._merged_bindings = cls._merge_bindings()
|
||||
cls._css_type_names = frozenset(css_type_names)
|
||||
|
||||
def get_component_styles(self, name: str) -> RenderStyles:
|
||||
@@ -205,6 +216,25 @@ class DOMNode(MessagePump):
|
||||
else:
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def _merge_bindings(cls) -> Bindings:
|
||||
"""Merge bindings from base classes.
|
||||
|
||||
Returns:
|
||||
Bindings: Merged bindings.
|
||||
"""
|
||||
bindings: list[Bindings] = []
|
||||
|
||||
for base in reversed(cls.__mro__):
|
||||
if issubclass(base, DOMNode):
|
||||
if not base._inherit_bindings:
|
||||
bindings.clear()
|
||||
bindings.append(Bindings(base.BINDINGS))
|
||||
keys = {}
|
||||
for bindings_ in bindings:
|
||||
keys.update(bindings_.keys)
|
||||
return Bindings(keys.values())
|
||||
|
||||
def _post_register(self, app: App) -> None:
|
||||
"""Called when the widget is registered
|
||||
|
||||
|
||||
@@ -174,6 +174,12 @@ class Widget(DOMNode):
|
||||
BINDINGS = [
|
||||
Binding("up", "scroll_up", "Scroll Up", show=False),
|
||||
Binding("down", "scroll_down", "Scroll Down", show=False),
|
||||
Binding("left", "scroll_left", "Scroll Up", show=False),
|
||||
Binding("right", "scroll_right", "Scroll Right", show=False),
|
||||
Binding("home", "scroll_home", "Scroll Home", show=False),
|
||||
Binding("end", "scroll_end", "Scroll End", show=False),
|
||||
Binding("pageup", "page_up", "Page Up", show=False),
|
||||
Binding("pagedown", "page_down", "Page Down", show=False),
|
||||
]
|
||||
|
||||
DEFAULT_CSS = """
|
||||
@@ -1816,9 +1822,13 @@ class Widget(DOMNode):
|
||||
can_focus: bool | None = None,
|
||||
can_focus_children: bool | None = None,
|
||||
inherit_css: bool = True,
|
||||
inherit_bindings: bool = True,
|
||||
) -> None:
|
||||
base = cls.__mro__[0]
|
||||
super().__init_subclass__(inherit_css=inherit_css)
|
||||
super().__init_subclass__(
|
||||
inherit_css=inherit_css,
|
||||
inherit_bindings=inherit_bindings,
|
||||
)
|
||||
if issubclass(base, Widget):
|
||||
cls.can_focus = base.can_focus if can_focus is None else can_focus
|
||||
cls.can_focus_children = (
|
||||
@@ -2345,53 +2355,21 @@ class Widget(DOMNode):
|
||||
def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None:
|
||||
self.scroll_to_region(message.region, animate=True)
|
||||
|
||||
def _key_home(self) -> bool:
|
||||
def action_scroll_home(self) -> None:
|
||||
if self._allow_scroll:
|
||||
self.scroll_home()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _key_end(self) -> bool:
|
||||
def action_scroll_end(self) -> None:
|
||||
if self._allow_scroll:
|
||||
self.scroll_end()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _key_left(self) -> bool:
|
||||
def action_scroll_left(self) -> None:
|
||||
if self.allow_horizontal_scroll:
|
||||
self.scroll_left()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _key_right(self) -> bool:
|
||||
def action_scroll_right(self) -> None:
|
||||
if self.allow_horizontal_scroll:
|
||||
self.scroll_right()
|
||||
return True
|
||||
return False
|
||||
|
||||
# def _key_down(self) -> bool:
|
||||
# if self.allow_vertical_scroll:
|
||||
# self.scroll_down()
|
||||
# return True
|
||||
# return False
|
||||
|
||||
# def _key_up(self) -> bool:
|
||||
# if self.allow_vertical_scroll:
|
||||
# self.scroll_up()
|
||||
# return True
|
||||
# return False
|
||||
|
||||
def _key_pagedown(self) -> bool:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_down()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _key_pageup(self) -> bool:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_up()
|
||||
return True
|
||||
return False
|
||||
|
||||
def action_scroll_up(self) -> None:
|
||||
if self.allow_vertical_scroll:
|
||||
@@ -2400,3 +2378,11 @@ class Widget(DOMNode):
|
||||
def action_scroll_down(self) -> None:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_down()
|
||||
|
||||
def action_page_down(self) -> None:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_down()
|
||||
|
||||
def action_page_up(self) -> None:
|
||||
if self.allow_vertical_scroll:
|
||||
self.scroll_page_up()
|
||||
|
||||
@@ -26,7 +26,7 @@ NodeID = NewType("NodeID", int)
|
||||
TreeDataType = TypeVar("TreeDataType")
|
||||
EventTreeDataType = TypeVar("EventTreeDataType")
|
||||
|
||||
LineCacheKey: TypeAlias = tuple[int | tuple[int, ...], ...]
|
||||
LineCacheKey: TypeAlias = tuple[int | tuple, ...]
|
||||
|
||||
TOGGLE_STYLE = Style.from_meta({"toggle": True})
|
||||
|
||||
@@ -199,9 +199,9 @@ class TreeNode(Generic[TreeDataType]):
|
||||
class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
|
||||
BINDINGS = [
|
||||
Binding("enter", "select_cursor", "Select", show=False),
|
||||
Binding("up", "cursor_up", "Cursor Up", show=False),
|
||||
Binding("down", "cursor_down", "Cursor Down", show=False),
|
||||
Binding("enter", "select_cursor", "Select", show=False),
|
||||
]
|
||||
|
||||
DEFAULT_CSS = """
|
||||
@@ -334,6 +334,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
"""TreeNode | Node: The currently selected node, or ``None`` if no selection."""
|
||||
return self._cursor_node
|
||||
|
||||
@property
|
||||
def last_line(self) -> int:
|
||||
return len(self._tree_lines) - 1
|
||||
|
||||
def process_label(self, label: TextType):
|
||||
"""Process a str or Text in to a label. Maybe overridden in a subclass to change modify how labels are rendered.
|
||||
|
||||
@@ -797,15 +801,37 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
|
||||
def action_cursor_up(self) -> None:
|
||||
if self.cursor_line == -1:
|
||||
self.cursor_line = len(self._tree_lines) - 1
|
||||
self.cursor_line = self.last_line
|
||||
else:
|
||||
self.cursor_line -= 1
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_cursor_down(self) -> None:
|
||||
if self.cursor_line == -1:
|
||||
self.cursor_line = 0
|
||||
self.cursor_line += 1
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_page_down(self) -> None:
|
||||
if self.cursor_line == -1:
|
||||
self.cursor_line = 0
|
||||
self.cursor_line += self.scrollable_content_region.height - 1
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_page_up(self) -> None:
|
||||
if self.cursor_line == -1:
|
||||
self.cursor_line = self.last_line
|
||||
self.cursor_line -= self.scrollable_content_region.height - 1
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_scroll_home(self) -> None:
|
||||
self.cursor_line = 0
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_scroll_end(self) -> None:
|
||||
self.cursor_line = self.last_line
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_select_cursor(self) -> None:
|
||||
try:
|
||||
line = self._tree_lines[self.cursor_line]
|
||||
|
||||
Reference in New Issue
Block a user