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
- Moved Ctrl+C, tab, and shift+tab to App BINDINGS
- Deprecated `PRIORITY_BINDINGS` class variable.
## [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.
See the previous section on [input](./input.md#bindings) for more information on bindings.
## 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.

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
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"
@@ -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,!,_"}
```
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
@@ -31,8 +31,7 @@ When you press a key, the app will receive the associated event and write it to
### 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
@@ -48,13 +47,13 @@ Many keys can also be combined with ++ctrl++ which will prefix the key with `ctr
#### 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`.
#### 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"`.
@@ -64,7 +63,7 @@ The `is_printable` attribute is a boolean which indicates if the key would typic
#### 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
@@ -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.
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
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

View File

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

View File

@@ -5,6 +5,7 @@ from typing import Iterable, MutableMapping
import rich.repr
from textual.keys import _character_to_key
from textual._typing import TypeAlias
BindingType: TypeAlias = "Binding | tuple[str, str, str]"
@@ -32,7 +33,7 @@ class Binding:
"""bool: Show the action in Footer, or False to hide."""
key_display: str | None = None
"""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?"""
@@ -43,13 +44,11 @@ class Bindings:
def __init__(
self,
bindings: Iterable[BindingType] | None = None,
default_priority: bool | None = None,
) -> None:
"""Initialise a collection of bindings.
Args:
bindings (Iterable[BindingType] | None, optional): An optional set of initial bindings.
default_priority (bool | None, optional): The default priority of the bindings.
Note:
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
# into a (potential) collection of Binding instances.
for key in binding.key.split(","):
key = key.strip()
if len(key) == 1:
key = _character_to_key(key)
yield Binding(
key=key.strip(),
action=binding.action,
description=binding.description,
show=binding.show,
key_display=binding.key_display,
priority=(
default_priority
if binding.priority is None
else binding.priority
),
priority=binding.priority,
)
self.keys: MutableMapping[str, Binding] = (

View File

@@ -92,9 +92,6 @@ class DOMNode(MessagePump):
# Virtual DOM nodes
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
BINDINGS: ClassVar[list[BindingType]] = []
@@ -233,13 +230,13 @@ class DOMNode(MessagePump):
for base in reversed(cls.__mro__):
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:
bindings.clear()
bindings.append(Bindings(base.__dict__.get("BINDINGS", []), priority))
bindings.append(
Bindings(
base.__dict__.get("BINDINGS", []),
)
)
keys = {}
for bindings_ in bindings:
keys.update(bindings_.keys)

View File

@@ -256,3 +256,14 @@ def _get_key_display(key: str) -> str:
if unicode_character.isprintable():
return unicode_character
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
# says otherwise, all screen-level bindings should be treated as having
# priority.
PRIORITY_BINDINGS = True
DEFAULT_CSS = """
Screen {

View File

@@ -57,7 +57,7 @@ async def test_just_app_no_bindings() -> None:
class AlphaBinding(App[None]):
"""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:
@@ -81,8 +81,7 @@ async def test_just_app_alpha_binding() -> None:
class LowAlphaBinding(App[None]):
"""An app with a simple low-priority alpha key binding."""
PRIORITY_BINDINGS = False
BINDINGS = [Binding("a", "a", "a")]
BINDINGS = [Binding("a", "a", "a", priority=False)]
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):
"""A screen with a simple alpha key binding."""
BINDINGS = [Binding("a", "a", "a")]
BINDINGS = [Binding("a", "a", "a", priority=True)]
class AppWithScreenThatHasABinding(App[None]):
@@ -144,8 +143,7 @@ async def test_app_screen_with_bindings() -> None:
class ScreenWithLowBindings(Screen):
"""A screen with a simple low-priority alpha key binding."""
PRIORITY_BINDINGS = False
BINDINGS = [Binding("a", "a", "a")]
BINDINGS = [Binding("a", "a", "a", priority=False)]
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
events = events[1:]
for index, character in enumerate(sequence[1:]):
assert events[index].char == character
assert events[index].character == character
@pytest.mark.parametrize(