Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 48 additions & 20 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ def _make_component_node(self, component: str) -> DOMNode:

# Generated list of bindings
_merged_bindings: ClassVar[BindingsMap] = BindingsMap()
_own_bindings: ClassVar[BindingsMap] = BindingsMap()
"""This class's own BINDINGS, parsed once when the class is created."""

_reactives: ClassVar[dict[str, Reactive]]
_reactive_accessors: ClassVar[dict[str, _StoredReactiveAccess]]
Expand Down Expand Up @@ -566,17 +568,55 @@ def declared_bindings(cls, mode: type[EditingMode]) -> tuple[Binding, ...]:
for declaration in family.declarations()
for binding in declaration.bindings_for(mode)),
*cls.native_bindings(mode),
*(projected for base in cls.__mro__
for binding in base.__dict__.get("_declared_key_bindings", ())
for projected in binding.for_mode(mode)),
*cls.method_bindings(mode),
)

@classmethod
def method_bindings(cls, mode: type[EditingMode]) -> tuple[Binding, ...]:
"""Keys each class in the MRO declares on its own methods."""
return tuple(projected for base in cls.__mro__
for binding in base.__dict__.get("_declared_key_bindings", ())
for projected in binding.for_mode(mode))

@classmethod
def class_bindings(cls, mode: type[EditingMode]) -> dict[str, list[Binding]]:
"""Each class's own keys replace its ancestors' bindings for those keys.

A class writes a key either in BINDINGS or by declaring it on a method; both are
that class's word on the key, so the most-derived class wins whichever form it
used (TextArea's declared End over ScrollableContainer's End BINDINGS).
"""
keys: dict[str, list[Binding]] = {}
for base in cls._binding_bases():
keys.update(base._own_bindings.project(
mode, base.__dict__.get("_declared_key_bindings", ())).key_to_bindings)
return keys

@classmethod
def _binding_bases(cls) -> list[type[DOMNode]]:
"""The DOMNode bases whose keys this class inherits, oldest first: the walk restarts
at any base that does not inherit its ancestors' bindings."""
bases: list[type[DOMNode]] = []
for base in reversed(cls.__mro__):
if issubclass(base, DOMNode):
if not base._inherit_bindings:
bases.clear()
bases.append(base)
return bases

@classmethod
@lru_cache(maxsize=None)
def _binding_defaults(cls, mode: type[EditingMode]) -> tuple[tuple[str, tuple[Binding, ...]], ...]:
"""Cache immutable projections, never a caller's mutable remapping table."""
projected = cls._merged_bindings.project(mode, cls.declared_bindings(mode))
return tuple((key, tuple(bindings)) for key, bindings in projected.key_to_bindings.items())
keys = {key: list(bindings) for key, bindings in cls.class_bindings(mode).items()}
# A method's key was placed, or replaced, with its class above; the families follow.
placed = cls.method_bindings(mode)
for binding in Binding.make_bindings(
declared for declared in cls.declared_bindings(mode) if declared not in placed):
for projected in binding.for_mode(mode):
if projected not in (bindings := keys.setdefault(projected.key, [])):
bindings.append(projected)
return tuple((key, tuple(bindings)) for key, bindings in keys.items())

@classmethod
def binding_defaults(cls, mode: type[EditingMode]) -> BindingsMap:
Expand Down Expand Up @@ -984,6 +1024,7 @@ def __init_subclass__(
cls._css_type_name = bases[0].__name__
for base in bases:
css_type_names.add(base.__name__)
cls._own_bindings = BindingsMap(cls.__dict__.get("BINDINGS", []))
cls._merged_bindings = cls._merge_bindings()
cls._css_type_names = frozenset(css_type_names)
compute_methods = frozenset(
Expand Down Expand Up @@ -1075,22 +1116,9 @@ def _merge_bindings(cls) -> BindingsMap:
Returns:
Merged bindings.
"""
bindings: list[BindingsMap] = []

for base in reversed(cls.__mro__):
if issubclass(base, DOMNode):
if not base._inherit_bindings:
bindings.clear()
bindings.append(
BindingsMap(
base.__dict__.get("BINDINGS", []),
)
)

keys: dict[str, list[Binding]] = {}
for bindings_ in bindings:
for key, key_bindings in bindings_.key_to_bindings.items():
keys[key] = key_bindings
for base in cls._binding_bases():
keys.update(base._own_bindings.key_to_bindings)

new_bindings = BindingsMap.from_keys(keys)
return new_bindings
Expand Down
3 changes: 2 additions & 1 deletion src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,8 @@ def update_suggestion(self) -> None:

@property
def allows_application_commands(self) -> bool:
return self.editing_mode.allows_application_commands
"""A read-only area takes no typed text, so the application's shortcuts reach past it."""
return self.read_only or self.editing_mode.allows_application_commands

def check_consume_key(self, key: str, character: str | None = None) -> bool:
"""Check if the widget may consume the given key.
Expand Down
Loading