Skip to content

Commit

Permalink
add inherited bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Nov 19, 2022
1 parent 811dcd8 commit b48a140
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,6 @@ def add_widget(

# Add top level (root) widget
add_widget(root, size.region, size.region, ((0,),), layer_order, size.region)
root.log(map)
return map, widgets

@property
Expand Down
36 changes: 33 additions & 3 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

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
Expand Down Expand Up @@ -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,
*,
Expand Down Expand Up @@ -127,7 +134,7 @@ def __init__(
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

Expand All @@ -152,12 +159,16 @@ def _automatic_refresh(self) -> None:
"""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:
Expand Down Expand Up @@ -205,6 +216,25 @@ def _css_bases(cls, base: Type[DOMNode]) -> Iterator[Type[DOMNode]]:
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
Expand Down
60 changes: 23 additions & 37 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down Expand Up @@ -1816,9 +1822,13 @@ def __init_subclass__(
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 = (
Expand Down Expand Up @@ -2345,53 +2355,21 @@ def _on_hide(self, event: events.Hide) -> None:
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:
Expand All @@ -2400,3 +2378,11 @@ def action_scroll_up(self) -> None:
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()
32 changes: 29 additions & 3 deletions src/textual/widgets/_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
TreeDataType = TypeVar("TreeDataType")
EventTreeDataType = TypeVar("EventTreeDataType")

LineCacheKey: TypeAlias = tuple[int | tuple[int, ...], ...]
LineCacheKey: TypeAlias = tuple[int | tuple, ...]

TOGGLE_STYLE = Style.from_meta({"toggle": True})

Expand Down Expand Up @@ -199,9 +199,9 @@ def add(
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 = """
Expand Down Expand Up @@ -334,6 +334,10 @@ def cursor_node(self) -> TreeNode[TreeDataType] | None:
"""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.
Expand Down Expand Up @@ -797,15 +801,37 @@ def _on_styles_updated(self) -> None:

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]
Expand Down

0 comments on commit b48a140

Please sign in to comment.