keys refactor

This commit is contained in:
Will McGugan
2022-12-20 21:18:10 +00:00
parent dd2b89bd5c
commit 5e2e0fe566
10 changed files with 94 additions and 84 deletions

View File

@@ -124,37 +124,9 @@ In the previous example if you wanted a link to set the background on the app ra
Textual supports the following builtin actions which are defined on the app.
### Bell
::: textual.app.App.action_bell
options:
show_root_heading: false
### Push screen
::: textual.app.App.action_push_screen
### Pop screen
::: textual.app.App.action_pop_screen
### Screenshot
::: textual.app.App.action_screenshot
### Switch screen
::: textual.app.App.action_switch_screen
### Toggle_dark
::: textual.app.App.action_toggle_dark
### Quit
::: textual.app.App.action_quit
- [action_bell][textual.app.App.action_bell]
- [action_push_screen][textual.app.App.action_push_screen]
- [action_pop_screen][textual.app.App.action_pop_screen]
- [action_switch_screen][textual.app.App.action_switch_screen]
- [action_screenshot][textual.app.App.action_screenshot]
- [action_toggle_dark][textual.app.App.action_toggle_dark]

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](./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. Let's write an app to show key events as you type.
=== "key01.py"
@@ -23,25 +23,53 @@ The most fundamental way to receive input is via [Key](./events/key) events. Let
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
```
Note the key event handler on the app which logs all key events. If you press any key it will show up on the screen.
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.
### Attributes
!!! tip
There are two main attributes on a key event. The `key` attribute is the _name_ of the key which may be a single character, or a longer identifier. Textual ensures that the `key` attribute could always be used in a method name.
For a more feature feature rich version of this example, run `textual keys` from the command line.
Key events also contain a `char` attribute which contains a single character if it is printable, or ``None`` if it is not printable (like a function key which has no corresponding character).
### Key Event
To illustrate the difference between `key` and `char`, try `key01.py` with the space key. You should see something like the following:
The key event contains a number of attributes which tell you what key (or keys) have been pressed.
```{.textual path="docs/examples/guide/input/key01.py", press="space,_"}
```
#### key
The `key` attribute is a string which identifies the key that was pressed. The value of `key` will be a single character for letter and numbers, or a longer identifier for other keys.
Some keys may be combined with ++shift++ key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the `key` attribute will be prefixed with `shift+`. For example ++shift+home++ will produce an event with `key="shift+home"`.
Many keys can also be combined with ++ctrl++ which will prefix the key with `ctrl+`. For instance ++ctrl+p++ will produce an event with `key="ctrl+p"`.
!!! warning
Not all keys combinations are supported in terminals, and some keys may be intercepted by your OS. If in doubt, run `textual keys` from the command line.
#### 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`.
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.
For example, ++ctrl+q++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`.
#### is_printable
The `is_printable` attribute is a boolean which indicates if the key would typically result in something that could be used in an input widget. If `is_printable` is `False` then the key is a control code or function key that you wouldn't expect to produce anything in an input.
#### 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"]`
Note that the `key` attribute contains the word "space" while the `char` attribute contains a literal space.
### Key methods
Textual offers a convenient way of handling specific keys. If you create a method beginning with `key_` followed by the name of a key, then that method will be called in response to the key.
Textual offers a convenient way of handling specific keys. If you create a method beginning with `key_` followed by the key name (the event's `name` attribute), then that method will be called in response to the key.
Let's add a key method to the example code.
@@ -134,7 +162,7 @@ Note how the footer displays bindings and makes them clickable.
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 priroty for all `BINDINGS`.
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

View File

@@ -127,7 +127,7 @@ def colors():
@run.command("keys")
def keys():
"""Show key events"""
"""Show key events."""
from textual.cli.previews import keys
keys.app.run()

View File

@@ -9,7 +9,7 @@ from textual.widgets import Button, Header, TextLog
INSTRUCTIONS = """\
Press some keys!
Because we want to display all the keys, Ctrl+C won't work for this example. Use the button below to quit.\
Because we want to display all the keys, ctrl+C won't quit this example. Use the Quit button below to exit the app.\
"""

View File

@@ -16,7 +16,6 @@ if TYPE_CHECKING:
from .timer import Timer as TimerClass
from .timer import TimerCallback
from .widget import Widget
import asyncio
@rich.repr.auto
@@ -183,7 +182,7 @@ class MouseRelease(Event, bubble=False):
class InputEvent(Event):
pass
"""Base class for input events."""
@rich.repr.auto
@@ -191,47 +190,54 @@ class Key(InputEvent):
"""Sent when the user hits a key on the keyboard.
Args:
sender (MessageTarget): The sender of the event (the App).
key (str): A key name (textual.keys.Keys).
char (str | None, optional): A printable character or None if it is not printable.
sender (MessageTarget): The sender of the event (always the App).
key (str): The key that was pressed.
char (str | None, optional): A printable character or ``None`` if it is not printable.
Attributes:
key_aliases (list[str]): The aliases for the key, including the key itself
"""
__slots__ = ["key", "char", "key_aliases"]
__slots__ = ["key", "character", "aliases"]
def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None:
super().__init__(sender)
self.key = key
self.char = (key if len(key) == 1 else None) if char is None else char
self.key_aliases = [_normalize_key(alias) for alias in _get_key_aliases(key)]
self.character = (key if len(key) == 1 else None) if char is None else char
self.aliases = _get_key_aliases(key)
def __rich_repr__(self) -> rich.repr.Result:
yield "key", self.key
yield "char", self.char
yield "character", self.character
yield "name", self.name
yield "is_printable", self.is_printable
yield "key_aliases", self.key_aliases, [self.key_name]
yield "aliases", self.aliases, [self.key]
@property
def key_name(self) -> str | None:
def name(self) -> str | None:
"""Name of a key suitable for use as a Python identifier."""
return _normalize_key(self.key)
return _key_to_identifier(self.key).lower()
@property
def name_aliases(self) -> list[str]:
"""The corresponding name for every alias in `aliases` list."""
return [_key_to_identifier(key) for key in self.aliases]
@property
def is_printable(self) -> bool:
"""Return True if the key is printable. Currently, we assume any key event that
isn't defined in key bindings is printable.
"""Check if the key is printable (produces a unicode character).
Returns:
bool: True if the key is printable.
"""
return False if self.char is None else self.char.isprintable()
return False if self.character is None else self.character.isprintable()
def _normalize_key(key: str) -> str:
def _key_to_identifier(key: str) -> str:
"""Convert the key string to a name suitable for use as a Python identifier."""
return key.replace("+", "_")
if len(key) == 1 and key.isupper():
key = f"upper_{key.lower()}"
return key.replace("+", "_").lower()
@rich.repr.auto

View File

@@ -70,10 +70,10 @@ class Keys(str, Enum):
ControlShift9 = "ctrl+shift+9"
ControlShift0 = "ctrl+shift+0"
ControlBackslash = "ctrl+\\"
ControlSquareClose = "ctrl+]"
ControlCircumflex = "ctrl+^"
ControlUnderscore = "ctrl+_"
ControlBackslash = "ctrl+backslash"
ControlSquareClose = "ctrl+right_square_bracked"
ControlCircumflex = "ctrl+circumflex_accent"
ControlUnderscore = "ctrl+underscore"
Left = "left"
Right = "right"

View File

@@ -24,7 +24,6 @@ class Message:
__slots__ = [
"sender",
"name",
"time",
"_forwarded",
"_no_default_action",
@@ -40,13 +39,14 @@ class Message:
def __init__(self, sender: MessageTarget) -> None:
self.sender = sender
self.name = camel_to_snake(self.__class__.__name__)
self.time = _clock.get_time_no_wait()
self._forwarded = False
self._no_default_action = False
self._stop_propagation = False
name = camel_to_snake(self.__class__.__name__)
self._handler_name = (
f"on_{self.namespace}_{self.name}" if self.namespace else f"on_{self.name}"
f"on_{self.namespace}_{name}" if self.namespace else f"on_{name}"
)
super().__init__()

View File

@@ -593,12 +593,12 @@ class MessagePump(metaclass=MessagePumpMeta):
handled = False
invoked_method = None
key_name = event.key_name
key_name = event.name
if not key_name:
return False
for key_alias in event.key_aliases:
key_method = get_key_handler(self, key_alias)
for key_method_name in event.name_aliases:
key_method = get_key_handler(self, key_method_name)
if key_method is not None:
if invoked_method:
_raise_duplicate_key_handlers_error(

View File

@@ -233,8 +233,8 @@ class Input(Widget, can_focus=True):
return
elif event.is_printable:
event.stop()
assert event.char is not None
self.insert_text_at_cursor(event.char)
assert event.character is not None
self.insert_text_at_cursor(event.character)
event.prevent_default()
def on_paste(self, event: events.Paste) -> None:

View File

@@ -66,6 +66,7 @@ class TextLog(ScrollView, can_focus=True):
content: RenderableType | object,
width: int | None = None,
expand: bool = False,
shrink: bool = True,
) -> None:
"""Write text or a rich renderable.
@@ -95,14 +96,17 @@ class TextLog(ScrollView, can_focus=True):
if isinstance(renderable, Text) and not self.wrap:
render_options = render_options.update(overflow="ignore", no_wrap=True)
if expand:
render_width = self.scrollable_content_region.width
else:
render_width = (
measure_renderables(console, render_options, [renderable]).maximum
if width is None
else width
)
render_width = measure_renderables(
console, render_options, [renderable]
).maximum
container_width = (
self.scrollable_content_region.width if width is None else width
)
if expand and render_width < container_width:
render_width = container_width
if shrink and render_width > container_width:
render_width = container_width
segments = self.app.console.render(
renderable, render_options.update_width(render_width)