docs and expanded keys

This commit is contained in:
Will McGugan
2022-12-21 11:38:20 +00:00
parent 5e2e0fe566
commit 7a46672d64
10 changed files with 54 additions and 49 deletions

View File

@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- Moved Ctrl+C, tab, and shift+tab to App BINDINGS - Moved Ctrl+C, tab, and shift+tab to App BINDINGS
- Deprecated `PRIORITY_BINDINGS` class variable.
## [0.7.0] - 2022-12-17 ## [0.7.0] - 2022-12-17

View File

@@ -90,6 +90,8 @@ Textual will run actions bound to keys. The following example adds key [bindings
If you run this example, you can change the background by pressing keys in addition to clicking links. If you run this example, you can change the background by pressing keys in addition to clicking links.
See the previous section on [input](./input.md#bindings) for more information on bindings.
## Namespaces ## Namespaces
Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions. Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions.

View File

@@ -10,7 +10,7 @@ This chapter will discuss how to make your app respond to input in the form of k
## Keyboard input ## Keyboard input
The most fundamental way to receive input is via [Key][textual.events.Key] events. Let's write an app to show key events as you type. The most fundamental way to receive input is via [Key][textual.events.Key] events which are sent to your app when the user presses a key. Let's write an app to show key events as you type.
=== "key01.py" === "key01.py"
@@ -23,7 +23,7 @@ The most fundamental way to receive input is via [Key][textual.events.Key] event
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"} ```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
``` ```
When you press a key, the app will receive the associated event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens. When you press a key, the app will receive the event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens.
!!! tip !!! tip
@@ -31,8 +31,7 @@ When you press a key, the app will receive the associated event and write it to
### Key Event ### Key Event
The key event contains a number of attributes which tell you what key (or keys) have been pressed. The key event contains following attributes which your app can use to know how to respond.
#### key #### key
@@ -48,13 +47,13 @@ Many keys can also be combined with ++ctrl++ which will prefix the key with `ctr
#### character #### character
If the key has an associated printable character then the `character` will contain a string containing a single unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`. If the key has an associated printable character then `character` will contain a string with a single unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`.
For example the ++p++ key will produce `character="p"` but ++f2++ will produce `character=None`. For example the ++p++ key will produce `character="p"` but ++f2++ will produce `character=None`.
#### name #### name
The `name` attribute is similar to `key` but unlike `key` is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` key attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. The `name` attribute is similar to `key` but, unlike `key`, is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names.
For example, ++ctrl+q++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`. For example, ++ctrl+q++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`.
@@ -64,7 +63,7 @@ The `is_printable` attribute is a boolean which indicates if the key would typic
#### aliases #### aliases
Some keys or combinations of keys can produce the same key. For instance, the ++tab++ key is indistinguishable from ++ctrl+i++ in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of ++tab++, the `aliases` attribute will contain `["tab", "ctrl+i"]` Some keys or combinations of keys can produce the same event. For instance, the ++tab++ key is indistinguishable from ++ctrl+i++ in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of ++tab++, the `aliases` attribute will contain `["tab", "ctrl+i"]`
### Key methods ### Key methods
@@ -155,24 +154,28 @@ Note how the footer displays bindings and makes them clickable.
Multiple keys can be bound to a single action by comma-separating them. Multiple keys can be bound to a single action by comma-separating them.
For example, `("r,t", "add_bar('red')", "Add Red")` means both ++r++ and ++t++ are bound to `add_bar('red')`. For example, `("r,t", "add_bar('red')", "Add Red")` means both ++r++ and ++t++ are bound to `add_bar('red')`.
!!! note
Ordinarily a binding on a focused widget has precedence over the same key binding at a higher level. However, bindings at the `App` or `Screen` level always have priority.
The priority of a single binding can be controlled with the `priority` parameter of a `Binding` instance. Set it to `True` to give it priority, or `False` to not.
The default priority of all bindings on a class can be controlled with the `PRIORITY_BINDINGS` class variable. Set it to `True` or `False` to set the default priority for all `BINDINGS`.
### Binding class ### Binding class
The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a [Binding][textual.binding.Binding] instance which exposes a few more options. The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a [Binding][textual.binding.Binding] instance which exposes a few more options.
### Why use bindings? ### Priority bindings
Bindings are particularly useful for configurable hot-keys. Bindings can also be inspected in widgets such as [Footer](../widgets/footer.md). Textual checks bindings for the focused widget first. If there is no matching binding, then the parent widget(s) will be checked. Occasionally, you may want bindings to be checked *before* the focused widget. Typically to create hot-keys.
You can create priority key bindings by setting `priority=True` on the Binding object. Textual uses this feature to add a default binding for ++ctrl+c++ so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority:
```python
BINDINGS = [
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
Binding("tab", "focus_next", "Focus Next", show=False),
Binding("shift+tab", "focus_previous", "Focus Previous", show=False),
]
```
### Show bindings
The [footer](../widgets/footer.md) widget can inspect bindings to display available keys. If you don't want a binding to display in the footer you can set `show=False`. The default bindings on App do this so that the standard ++ctrl+c++, ++tab++ and ++shift+tab++ bindings don't typically appear in the footer.
In a future version of Textual it will also be possible to specify bindings in a configuration file, which will allow users to override app bindings.
## Mouse Input ## Mouse Input

View File

@@ -232,8 +232,6 @@ class App(Generic[ReturnType], DOMNode):
} }
""" """
PRIORITY_BINDINGS = True
SCREENS: dict[str, Screen | Callable[[], Screen]] = {} SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
_BASE_PATH: str | None = None _BASE_PATH: str | None = None
CSS_PATH: CSSPathType = None CSS_PATH: CSSPathType = None
@@ -242,10 +240,8 @@ class App(Generic[ReturnType], DOMNode):
BINDINGS = [ BINDINGS = [
Binding("ctrl+c", "quit", "Quit", show=False, priority=True), Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
Binding("tab", "focus_next", "Focus Next", show=False, priority=False), Binding("tab", "focus_next", "Focus Next", show=False),
Binding( Binding("shift+tab", "focus_previous", "Focus Previous", show=False),
"shift+tab", "focus_previous", "Focus Previous", show=False, priority=False
),
] ]
title: Reactive[str] = Reactive("") title: Reactive[str] = Reactive("")

View File

@@ -5,6 +5,7 @@ from typing import Iterable, MutableMapping
import rich.repr import rich.repr
from textual.keys import _character_to_key
from textual._typing import TypeAlias from textual._typing import TypeAlias
BindingType: TypeAlias = "Binding | tuple[str, str, str]" BindingType: TypeAlias = "Binding | tuple[str, str, str]"
@@ -32,7 +33,7 @@ class Binding:
"""bool: Show the action in Footer, or False to hide.""" """bool: Show the action in Footer, or False to hide."""
key_display: str | None = None key_display: str | None = None
"""str | None: How the key should be shown in footer.""" """str | None: How the key should be shown in footer."""
priority: bool | None = None priority: bool = False
"""bool | None: Is this a priority binding, checked form app down to focused widget?""" """bool | None: Is this a priority binding, checked form app down to focused widget?"""
@@ -43,13 +44,11 @@ class Bindings:
def __init__( def __init__(
self, self,
bindings: Iterable[BindingType] | None = None, bindings: Iterable[BindingType] | None = None,
default_priority: bool | None = None,
) -> None: ) -> None:
"""Initialise a collection of bindings. """Initialise a collection of bindings.
Args: Args:
bindings (Iterable[BindingType] | None, optional): An optional set of initial bindings. bindings (Iterable[BindingType] | None, optional): An optional set of initial bindings.
default_priority (bool | None, optional): The default priority of the bindings.
Note: Note:
The iterable of bindings can contain either a `Binding` The iterable of bindings can contain either a `Binding`
@@ -71,17 +70,16 @@ class Bindings:
# be a list of keys, so now we unroll that single Binding # be a list of keys, so now we unroll that single Binding
# into a (potential) collection of Binding instances. # into a (potential) collection of Binding instances.
for key in binding.key.split(","): for key in binding.key.split(","):
key = key.strip()
if len(key) == 1:
key = _character_to_key(key)
yield Binding( yield Binding(
key=key.strip(), key=key.strip(),
action=binding.action, action=binding.action,
description=binding.description, description=binding.description,
show=binding.show, show=binding.show,
key_display=binding.key_display, key_display=binding.key_display,
priority=( priority=binding.priority,
default_priority
if binding.priority is None
else binding.priority
),
) )
self.keys: MutableMapping[str, Binding] = ( self.keys: MutableMapping[str, Binding] = (

View File

@@ -92,9 +92,6 @@ class DOMNode(MessagePump):
# Virtual DOM nodes # Virtual DOM nodes
COMPONENT_CLASSES: ClassVar[set[str]] = set() COMPONENT_CLASSES: ClassVar[set[str]] = set()
# Should the content of BINDINGS be treated as priority bindings?
PRIORITY_BINDINGS: ClassVar[bool] = False
# Mapping of key bindings # Mapping of key bindings
BINDINGS: ClassVar[list[BindingType]] = [] BINDINGS: ClassVar[list[BindingType]] = []
@@ -233,13 +230,13 @@ class DOMNode(MessagePump):
for base in reversed(cls.__mro__): for base in reversed(cls.__mro__):
if issubclass(base, DOMNode): if issubclass(base, DOMNode):
# See if the current class wants to set the bindings as
# priority bindings. If it doesn't have that property on the
# class, go with what we saw last.
priority = base.__dict__.get("PRIORITY_BINDINGS", priority)
if not base._inherit_bindings: if not base._inherit_bindings:
bindings.clear() bindings.clear()
bindings.append(Bindings(base.__dict__.get("BINDINGS", []), priority)) bindings.append(
Bindings(
base.__dict__.get("BINDINGS", []),
)
)
keys = {} keys = {}
for bindings_ in bindings: for bindings_ in bindings:
keys.update(bindings_.keys) keys.update(bindings_.keys)

View File

@@ -256,3 +256,14 @@ def _get_key_display(key: str) -> str:
if unicode_character.isprintable(): if unicode_character.isprintable():
return unicode_character return unicode_character
return upper_original return upper_original
def _character_to_key(character: str) -> str:
"""Convert a single character to a key value."""
assert len(character) == 1
if not character.isalnum():
key = unicodedata.name(character).lower().replace("-", "_").replace(" ", "_")
else:
key = character
key = KEY_NAME_REPLACEMENTS.get(key, key)
return key

View File

@@ -29,7 +29,6 @@ class Screen(Widget):
# The screen is a special case and unless a class that inherits from us # The screen is a special case and unless a class that inherits from us
# says otherwise, all screen-level bindings should be treated as having # says otherwise, all screen-level bindings should be treated as having
# priority. # priority.
PRIORITY_BINDINGS = True
DEFAULT_CSS = """ DEFAULT_CSS = """
Screen { Screen {

View File

@@ -57,7 +57,7 @@ async def test_just_app_no_bindings() -> None:
class AlphaBinding(App[None]): class AlphaBinding(App[None]):
"""An app with a simple alpha key binding.""" """An app with a simple alpha key binding."""
BINDINGS = [Binding("a", "a", "a")] BINDINGS = [Binding("a", "a", "a", priority=True)]
async def test_just_app_alpha_binding() -> None: async def test_just_app_alpha_binding() -> None:
@@ -81,8 +81,7 @@ async def test_just_app_alpha_binding() -> None:
class LowAlphaBinding(App[None]): class LowAlphaBinding(App[None]):
"""An app with a simple low-priority alpha key binding.""" """An app with a simple low-priority alpha key binding."""
PRIORITY_BINDINGS = False BINDINGS = [Binding("a", "a", "a", priority=False)]
BINDINGS = [Binding("a", "a", "a")]
async def test_just_app_low_priority_alpha_binding() -> None: async def test_just_app_low_priority_alpha_binding() -> None:
@@ -106,7 +105,7 @@ async def test_just_app_low_priority_alpha_binding() -> None:
class ScreenWithBindings(Screen): class ScreenWithBindings(Screen):
"""A screen with a simple alpha key binding.""" """A screen with a simple alpha key binding."""
BINDINGS = [Binding("a", "a", "a")] BINDINGS = [Binding("a", "a", "a", priority=True)]
class AppWithScreenThatHasABinding(App[None]): class AppWithScreenThatHasABinding(App[None]):
@@ -144,8 +143,7 @@ async def test_app_screen_with_bindings() -> None:
class ScreenWithLowBindings(Screen): class ScreenWithLowBindings(Screen):
"""A screen with a simple low-priority alpha key binding.""" """A screen with a simple low-priority alpha key binding."""
PRIORITY_BINDINGS = False BINDINGS = [Binding("a", "a", "a", priority=False)]
BINDINGS = [Binding("a", "a", "a")]
class AppWithScreenThatHasALowBinding(App[None]): class AppWithScreenThatHasALowBinding(App[None]):

View File

@@ -106,7 +106,7 @@ def test_cant_match_escape_sequence_too_long(parser):
# The rest of the characters correspond to the expected key presses # The rest of the characters correspond to the expected key presses
events = events[1:] events = events[1:]
for index, character in enumerate(sequence[1:]): for index, character in enumerate(sequence[1:]):
assert events[index].char == character assert events[index].character == character
@pytest.mark.parametrize( @pytest.mark.parametrize(