Skip to content

Commit

Permalink
Merge pull request #4890 from Textualize/fix-cp-binding
Browse files Browse the repository at this point in the history
fix command palette key
  • Loading branch information
willmcgugan authored Aug 20, 2024
2 parents 8a1c603 + 87167da commit 424ef64
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 229 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added "Show keys" option to system commands to show a summary of key bindings. https://github.com/Textualize/textual/pull/4876
- Added "split" CSS style, currently undocumented, and may change. https://github.com/Textualize/textual/pull/4876
- Added `Region.get_spacing_between` https://github.com/Textualize/textual/pull/4876
- Added `App.COMMAND_PALETTE_KEY` to change default command palette key binding https://github.com/Textualize/textual/pull/4867
- Added `App.get_key_display` https://github.com/Textualize/textual/pull/4890

### Changed

- Removed caps_lock and num_lock modifiers https://github.com/Textualize/textual/pull/4861
- Keys such as escape and space are now displayed in lower case in footer https://github.com/Textualize/textual/pull/4876
- Changed default command palette binding to `ctrl+p` https://github.com/Textualize/textual/pull/4867
- Removed `ctrl_to_caret` and `upper_case_keys` from Footer. These can be implemented in `App.get_key_display`.

### Fixed

Expand Down
74 changes: 47 additions & 27 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@
from .keys import (
REPLACED_KEYS,
_character_to_key,
_get_key_display,
_get_unicode_name_from_key,
format_key,
)
from .messages import CallbackType, Prune
from .notifications import Notification, Notifications, Notify, SeverityLevel
Expand Down Expand Up @@ -367,18 +367,13 @@ class MyApp(App[None]):
"""

COMMAND_PALETTE_BINDING: ClassVar[str] = "ctrl+p"
"""The key that launches the command palette (if enabled)."""
"""The key that launches the command palette (if enabled by [`App.ENABLE_COMMAND_PALETTE`][textual.app.App.ENABLE_COMMAND_PALETTE])."""

COMMAND_PALETTE_DISPLAY: ClassVar[str | None] = None
"""How the command palette key should be displayed in the footer (or `None` for default)."""

BINDINGS: ClassVar[list[BindingType]] = [
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
Binding(
COMMAND_PALETTE_BINDING,
"command_palette",
"palette",
show=False,
priority=True,
tooltip="Open command palette",
),
Binding("ctrl+c", "quit", "Quit", show=False, priority=True)
]
"""The default key bindings."""

Expand Down Expand Up @@ -650,6 +645,23 @@ def __init__(
# Size of previous inline update
self._previous_inline_height: int | None = None

if self.ENABLE_COMMAND_PALETTE:
for _key, binding in self._bindings:
if binding.action in {"command_palette", "app.command_palette"}:
break
else:
self._bindings._add_binding(
Binding(
self.COMMAND_PALETTE_BINDING,
"command_palette",
"palette",
show=False,
key_display=self.COMMAND_PALETTE_DISPLAY,
priority=True,
tooltip="Open command palette",
)
)

def validate_title(self, title: Any) -> str:
"""Make sure the title is set to a string."""
return str(title)
Expand Down Expand Up @@ -1319,27 +1331,35 @@ def bind(
keys, action, description, show=show, key_display=key_display
)

def get_key_display(
self, key: str, upper_case_keys: bool = False, ctrl_to_caret: bool = True
) -> str:
"""For a given key, return how it should be displayed in an app
(e.g. in the Footer widget).
By key, we refer to the string used in the "key" argument for
a Binding instance. By overriding this method, you can ensure that
keys are displayed consistently throughout your app, without
needing to add a key_display to every binding.
def get_key_display(self, binding: Binding) -> str:
"""Format a bound key for display in footer / key panel etc.
!!! note
You can implement this in a subclass if you want to change how keys are displayed in your app.
Args:
key: The binding key string.
upper_case_keys: Upper case printable keys.
ctrl_to_caret: Replace `ctrl+` with `^`.
binding: A Binding.
Returns:
The display string for the input key.
A string used to represent the key.
"""
return _get_key_display(
key, upper_case_keys=upper_case_keys, ctrl_to_caret=ctrl_to_caret
)
# Dev has overridden the key display, so use that
if binding.key_display:
return binding.key_display

# Extract modifiers
modifiers, key = binding.parse_key()

# Format the key (replace unicode names with character)
key = format_key(key)

# Convert ctrl modifier to caret
if "ctrl" in modifiers:
modifiers.pop(modifiers.index("ctrl"))
key = f"^{key}"
# Join everything with +
key_tokens = modifiers + [key]
return "+".join(key_tokens)

async def _press_keys(self, keys: Iterable[str]) -> None:
"""A task to send key events."""
Expand Down
17 changes: 17 additions & 0 deletions src/textual/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ class Binding:
tooltip: str = ""
"""Optional tooltip to show in footer."""

def parse_key(self) -> tuple[list[str], str]:
"""Parse a key in to a list of modifiers, and the actual key.
Returns:
A tuple of (MODIFIER LIST, KEY).
"""
*modifiers, key = self.key.split("+")
return modifiers, key


class ActiveBinding(NamedTuple):
"""Information about an active binding (returned from [active_bindings][textual.screen.Screen.active_bindings])."""
Expand Down Expand Up @@ -123,6 +132,14 @@ def make_bindings(bindings: Iterable[BindingType]) -> Iterable[Binding]:
for binding in make_bindings(bindings or {}):
self.key_to_bindings.setdefault(binding.key, []).append(binding)

def _add_binding(self, binding: Binding) -> None:
"""Add a new binding.
Args:
binding: New Binding to add.
"""
self.key_to_bindings.setdefault(binding.key, []).append(binding)

def __iter__(self) -> Iterator[tuple[str, Binding]]:
"""Iterating produces a sequence of (KEY, BINDING) tuples."""
return iter(
Expand Down
28 changes: 6 additions & 22 deletions src/textual/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,24 +281,10 @@ def _get_key_aliases(key: str) -> list[str]:
return [key] + KEY_ALIASES.get(key, [])


def _get_key_display(
key: str,
upper_case_keys: bool = False,
ctrl_to_caret: bool = True,
) -> str:
def format_key(key: str) -> str:
"""Given a key (i.e. the `key` string argument to Binding __init__),
return the value that should be displayed in the app when referring
to this key (e.g. in the Footer widget)."""
if "+" in key:
key_components = key.split("+")
caret = False
if ctrl_to_caret and "ctrl" in key_components:
key_components.remove("ctrl")
caret = True
key_display = ("^" if caret else "") + "+".join(
[_get_key_display(key) for key in key_components]
)
return key_display

display_alias = KEY_DISPLAY_ALIASES.get(key)
if display_alias:
Expand All @@ -307,14 +293,12 @@ def _get_key_display(
original_key = REPLACED_KEYS.get(key, key)
tentative_unicode_name = _get_unicode_name_from_key(original_key)
try:
unicode_character = unicodedata.lookup(tentative_unicode_name)
unicode_name = unicodedata.lookup(tentative_unicode_name)
except KeyError:
return tentative_unicode_name

# Check if printable. `delete` for example maps to a control sequence
# which we don't want to write to the terminal.
if unicode_character.isprintable():
return unicode_character.upper() if upper_case_keys else unicode_character
pass
else:
if unicode_name.isprintable():
return unicode_name
return tentative_unicode_name


Expand Down
24 changes: 3 additions & 21 deletions src/textual/widgets/_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ class FooterKey(Widget):
}
"""

upper_case_keys = reactive(False)
ctrl_to_caret = reactive(True)
compact = reactive(True)

def __init__(
Expand Down Expand Up @@ -106,10 +104,6 @@ def render(self) -> Text:
description_padding = self.get_component_styles(
"footer-key--description"
).padding
if self.upper_case_keys:
key_display = key_display.upper()
if self.ctrl_to_caret and key_display.lower().startswith("ctrl+"):
key_display = "^" + key_display.split("+", 1)[1]
description = self.description
label_text = Text.assemble(
(
Expand Down Expand Up @@ -158,10 +152,6 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
}
"""

upper_case_keys = reactive(False)
"""Upper case key display."""
ctrl_to_caret = reactive(True)
"""Convert 'ctrl+' prefix to '^'."""
compact = reactive(False)
"""Display in compact style."""
_bindings_ready = reactive(False, repaint=False)
Expand All @@ -176,8 +166,6 @@ def __init__(
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
upper_case_keys: bool = False,
ctrl_to_caret: bool = True,
show_command_palette: bool = True,
) -> None:
"""A footer to show key bindings.
Expand All @@ -199,8 +187,6 @@ def __init__(
classes=classes,
disabled=disabled,
)
self.set_reactive(Footer.upper_case_keys, upper_case_keys)
self.set_reactive(Footer.ctrl_to_caret, ctrl_to_caret)
self.set_reactive(Footer.show_command_palette, show_command_palette)

def compose(self) -> ComposeResult:
Expand All @@ -221,16 +207,12 @@ def compose(self) -> ComposeResult:
binding, enabled, tooltip = multi_bindings[0]
yield FooterKey(
binding.key,
binding.key_display or self.app.get_key_display(binding.key),
self.app.get_key_display(binding),
binding.description,
binding.action,
disabled=not enabled,
tooltip=tooltip,
).data_bind(
Footer.upper_case_keys,
Footer.ctrl_to_caret,
Footer.compact,
)
).data_bind(Footer.compact)
if self.show_command_palette and self.app.ENABLE_COMMAND_PALETTE:
for key, binding in self.app._bindings:
if binding.action in (
Expand All @@ -239,7 +221,7 @@ def compose(self) -> ComposeResult:
):
yield FooterKey(
key,
binding.key_display or binding.key,
self.app.get_key_display(binding),
binding.description,
binding.action,
classes="-command-palette",
Expand Down
26 changes: 2 additions & 24 deletions src/textual/widgets/_key_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ..app import ComposeResult
from ..binding import Binding
from ..containers import VerticalScroll
from ..reactive import reactive
from ..widgets import Static

if TYPE_CHECKING:
Expand All @@ -28,11 +27,6 @@ class BindingsTable(Static):
}
"""

upper_case_keys = reactive(False)
"""Upper case key display."""
ctrl_to_caret = reactive(True)
"""Convert 'ctrl+' prefix to '^'."""

def render_bindings_table(self) -> Table:
"""Render a table with all the key bindings.
Expand Down Expand Up @@ -66,15 +60,7 @@ def render_description(binding: Binding) -> Text:
for multi_bindings in action_to_bindings.values():
binding, enabled, tooltip = multi_bindings[0]
table.add_row(
Text(
binding.key_display
or self.app.get_key_display(
binding.key,
upper_case_keys=self.upper_case_keys,
ctrl_to_caret=self.ctrl_to_caret,
),
style=key_style,
),
Text(self.app.get_key_display(binding), style=key_style),
render_description(binding),
)

Expand Down Expand Up @@ -113,18 +99,10 @@ class KeyPanel(VerticalScroll, can_focus=False):
}
"""

upper_case_keys = reactive(False)
"""Upper case key display."""
ctrl_to_caret = reactive(True)
"""Convert 'ctrl+' prefix to '^'."""

DEFAULT_CLASSES = "-textual-system"

def compose(self) -> ComposeResult:
yield BindingsTable(shrink=True, expand=False).data_bind(
KeyPanel.upper_case_keys,
KeyPanel.ctrl_to_caret,
)
yield BindingsTable(shrink=True, expand=False)

async def on_mount(self) -> None:
async def bindings_changed(screen: Screen) -> None:
Expand Down
Loading

0 comments on commit 424ef64

Please sign in to comment.