Skip to content

Commit

Permalink
Merge pull request #4940 from Textualize/check-consume-key
Browse files Browse the repository at this point in the history
add check_consume_key
  • Loading branch information
willmcgugan authored Aug 27, 2024
2 parents 6aebb0e + c45e560 commit 376cf0f
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 81 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added `DOMNode.check_consume_key` https://github.com/Textualize/textual/pull/4940

### Changed

- KeyPanel will show multiple keys if bound to the same action https://github.com/Textualize/textual/pull/4940

## [0.78.0] - 2024-08-27

Expand Down
17 changes: 17 additions & 0 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,23 @@ def rich_style(self) -> Style:
)
return style

def check_consume_key(self, key: str) -> bool:
"""Check if the widget may consume the given key.
This should be implemented in widgets that handle [`Key`][textual.events.Key] events and
stop propagation (such as Input and TextArea).
Implementing this method will hide key bindings from the footer and key panel that would
be *consumed* by the focused widget.
Args:
key: A key identifier.
Returns:
`True` if the widget may capture the key in its `Key` event handler, or `False` if it won't.
"""
return False

def _get_title_style_information(
self, background: Color
) -> tuple[Color, Color, Style]:
Expand Down
18 changes: 14 additions & 4 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,21 +308,32 @@ def _watch_maximized(
@property
def _binding_chain(self) -> list[tuple[DOMNode, BindingsMap]]:
"""Binding chain from this screen."""

focused = self.focused
if focused is not None and focused.loading:
focused = None
namespace_bindings: list[tuple[DOMNode, BindingsMap]]

if focused is None:
namespace_bindings = [
(self, self._bindings),
(self.app, self.app._bindings),
(self, self._bindings.copy()),
(self.app, self.app._bindings.copy()),
]
else:
namespace_bindings = [
(node, node._bindings) for node in focused.ancestors_with_self
(node, node._bindings.copy()) for node in focused.ancestors_with_self
]

# Filter out bindings that could be captures by widgets (such as Input, TextArea)
filter_namespaces: list[DOMNode] = []
for namespace, bindings_map in namespace_bindings:
for filter_namespace in filter_namespaces:
check_consume_key = filter_namespace.check_consume_key
for key in list(bindings_map.key_to_bindings):
if check_consume_key(key):
del bindings_map.key_to_bindings[key]
filter_namespaces.append(namespace)

return namespace_bindings

@property
Expand All @@ -346,7 +357,6 @@ def active_bindings(self) -> dict[str, ActiveBinding]:
Returns:
A map of keys to a tuple containing (NAMESPACE, BINDING, ENABLED).
"""

bindings_map: dict[str, ActiveBinding] = {}
for namespace, bindings in self._modal_binding_chain:
for key, binding in bindings:
Expand Down
13 changes: 13 additions & 0 deletions src/textual/widgets/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,19 @@ def _cursor_at_end(self) -> bool:
"""Flag to indicate if the cursor is at the end"""
return self.cursor_position >= len(self.value)

def check_consume_key(self, key: str) -> bool:
"""Check if the widget may consume the given key.
As an input we are expecting to capture printable keys.
Args:
key: A key identifier.
Returns:
`True` if the widget may capture the key in it's `Key` message, or `False` if it won't.
"""
return len(key) == 1 and key.isprintable()

def validate_cursor_position(self, cursor_position: int) -> int:
return min(max(0, cursor_position), len(self.value))

Expand Down
6 changes: 5 additions & 1 deletion src/textual/widgets/_key_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,14 @@ def render_description(binding: Binding) -> Text:
text.append(binding.tooltip, "dim")
return text

get_key_display = self.app.get_key_display
for multi_bindings in action_to_bindings.values():
binding, enabled, tooltip = multi_bindings[0]
key_display = " ".join(
get_key_display(binding) for binding, _, _ in multi_bindings
)
table.add_row(
Text(self.app.get_key_display(binding), style=key_style),
Text(key_display, style=key_style),
render_description(binding),
)
if namespace != previous_namespace:
Expand Down
21 changes: 19 additions & 2 deletions src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,29 @@ def _get_builtin_highlight_query(language_name: str) -> str:
Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm"
)
highlight_query = highlight_query_path.read_text()
except OSError as e:
log.warning(f"Unable to load highlight query. {e}")
except OSError as error:
log.warning(f"Unable to load highlight query. {error}")
highlight_query = ""

return highlight_query

def check_consume_key(self, key: str) -> bool:
"""Check if the widget may consume the given key.
As a textarea we are expecting to capture printable keys.
Args:
key: A key identifier.
Returns:
`True` if the widget may capture the key in it's `Key` message, or `False` if it won't.
"""
if self.read_only:
return False
if self.tab_behavior == "indent" and key == "tab":
return True
return len(key) == 1 and key.isprintable()

def _build_highlight_map(self) -> None:
"""Query the tree for ranges to highlights, and update the internal highlights mapping."""
highlights = self._highlights
Expand Down
Loading

0 comments on commit 376cf0f

Please sign in to comment.