mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -10,14 +10,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Fixed
|
||||
|
||||
- Fixed issues with nested auto dimensions https://github.com/Textualize/textual/issues/1402
|
||||
- Fixed watch method incorrectly running on first set when value hasn't changed and init=False https://github.com/Textualize/textual/pull/1367
|
||||
|
||||
### Added
|
||||
|
||||
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings.
|
||||
- Added `textual keys` preview.
|
||||
|
||||
### Fixed
|
||||
### Changed
|
||||
|
||||
- Fixed watch method incorrectly running on first set when value hasnt changed and init=False https://github.com/Textualize/textual/pull/1367
|
||||
- Moved Ctrl+C, tab, and shift+tab to App BINDINGS
|
||||
|
||||
## [0.7.0] - 2022-12-17
|
||||
|
||||
|
||||
@@ -240,6 +240,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
TITLE: str | None = None
|
||||
SUB_TITLE: str | None = None
|
||||
|
||||
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
|
||||
),
|
||||
]
|
||||
|
||||
title: Reactive[str] = Reactive("")
|
||||
sub_title: Reactive[str] = Reactive("")
|
||||
dark: Reactive[bool] = Reactive(True)
|
||||
@@ -301,7 +309,6 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
self._logger = Logger(self._log)
|
||||
|
||||
self._bindings.bind("ctrl+c", "quit", show=False, priority=True)
|
||||
self._refresh_required = False
|
||||
|
||||
self.design = DEFAULT_COLORS
|
||||
@@ -1898,13 +1905,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
message.stop()
|
||||
|
||||
async def _on_key(self, event: events.Key) -> None:
|
||||
if event.key == "tab":
|
||||
self.screen.focus_next()
|
||||
elif event.key == "shift+tab":
|
||||
self.screen.focus_previous()
|
||||
else:
|
||||
if not (await self.check_bindings(event.key)):
|
||||
await self.dispatch_key(event)
|
||||
if not (await self.check_bindings(event.key)):
|
||||
await self.dispatch_key(event)
|
||||
|
||||
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
log("shutdown request")
|
||||
@@ -2124,6 +2126,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
async def action_toggle_class(self, selector: str, class_name: str) -> None:
|
||||
self.screen.query(selector).toggle_class(class_name)
|
||||
|
||||
def action_focus_next(self) -> None:
|
||||
"""Focus the next widget."""
|
||||
self.screen.focus_next()
|
||||
|
||||
def action_focus_previous(self) -> None:
|
||||
"""Focus the previous widget."""
|
||||
self.screen.focus_previous()
|
||||
|
||||
def _on_terminal_supports_synchronized_output(
|
||||
self, message: messages.TerminalSupportsSynchronizedOutput
|
||||
) -> None:
|
||||
|
||||
@@ -123,3 +123,11 @@ def colors():
|
||||
from textual.cli.previews import colors
|
||||
|
||||
colors.app.run()
|
||||
|
||||
|
||||
@run.command("keys")
|
||||
def keys():
|
||||
"""Show key events"""
|
||||
from textual.cli.previews import keys
|
||||
|
||||
keys.app.run()
|
||||
|
||||
60
src/textual/cli/previews/keys.py
Normal file
60
src/textual/cli/previews/keys.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from rich.panel import Panel
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual import events
|
||||
from textual.containers import Horizontal
|
||||
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.\
|
||||
"""
|
||||
|
||||
|
||||
class KeyLog(TextLog, inherit_bindings=False):
|
||||
"""We don't want to handle scroll keys."""
|
||||
|
||||
|
||||
class KeysApp(App, inherit_bindings=False):
|
||||
"""Show key events in a text log."""
|
||||
|
||||
TITLE = "Textual Keys"
|
||||
BINDINGS = [("c", "clear", "Clear")]
|
||||
CSS = """
|
||||
#buttons {
|
||||
dock: bottom;
|
||||
height: 3;
|
||||
}
|
||||
Button {
|
||||
width: 1fr;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Horizontal(
|
||||
Button("Clear", id="clear", variant="warning"),
|
||||
Button("Quit", id="quit", variant="error"),
|
||||
id="buttons",
|
||||
)
|
||||
yield KeyLog()
|
||||
|
||||
def on_ready(self) -> None:
|
||||
self.query_one(KeyLog).write(Panel(INSTRUCTIONS), expand=True)
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
self.query_one(KeyLog).write(event)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "quit":
|
||||
self.exit()
|
||||
elif event.button.id == "clear":
|
||||
self.query_one(KeyLog).clear()
|
||||
|
||||
|
||||
app = KeysApp()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -22,7 +22,7 @@ from rich.tree import Tree
|
||||
|
||||
from ._context import NoActiveAppError
|
||||
from ._node_list import NodeList
|
||||
from .binding import Binding, Bindings, BindingType
|
||||
from .binding import Bindings, BindingType
|
||||
from .color import BLACK, WHITE, Color
|
||||
from .css._error_tools import friendly_list
|
||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
|
||||
@@ -199,7 +199,7 @@ class Key(InputEvent):
|
||||
key_aliases (list[str]): The aliases for the key, including the key itself
|
||||
"""
|
||||
|
||||
__slots__ = ["key", "char"]
|
||||
__slots__ = ["key", "char", "key_aliases"]
|
||||
|
||||
def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None:
|
||||
super().__init__(sender)
|
||||
@@ -209,7 +209,9 @@ class Key(InputEvent):
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "key", self.key
|
||||
yield "char", self.char, None
|
||||
yield "char", self.char
|
||||
yield "is_printable", self.is_printable
|
||||
yield "key_aliases", self.key_aliases, [self.key_name]
|
||||
|
||||
@property
|
||||
def key_name(self) -> str | None:
|
||||
|
||||
@@ -61,11 +61,18 @@ class TextLog(ScrollView, can_focus=True):
|
||||
def _on_styles_updated(self) -> None:
|
||||
self._line_cache.clear()
|
||||
|
||||
def write(self, content: RenderableType | object) -> None:
|
||||
def write(
|
||||
self,
|
||||
content: RenderableType | object,
|
||||
width: int | None = None,
|
||||
expand: bool = False,
|
||||
) -> None:
|
||||
"""Write text or a rich renderable.
|
||||
|
||||
Args:
|
||||
content (RenderableType): Rich renderable (or text).
|
||||
width (int): Width to render or None to use optimal width. Defaults to None.
|
||||
expand (bool): Enable expand to widget width, or False to use `width`.
|
||||
"""
|
||||
|
||||
renderable: RenderableType
|
||||
@@ -88,13 +95,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)
|
||||
|
||||
width = max(
|
||||
self.min_width,
|
||||
measure_renderables(console, render_options, [renderable]).maximum,
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
segments = self.app.console.render(
|
||||
renderable, render_options.update_width(width)
|
||||
renderable, render_options.update_width(render_width)
|
||||
)
|
||||
lines = list(Segment.split_lines(segments))
|
||||
if not lines:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,8 +39,10 @@ class NoBindings(App[None]):
|
||||
async def test_just_app_no_bindings() -> None:
|
||||
"""An app with no bindings should have no bindings, other than ctrl+c."""
|
||||
async with NoBindings().run_test() as pilot:
|
||||
assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c"]
|
||||
assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c", "tab", "shift+tab"]
|
||||
assert pilot.app._bindings.get_key("ctrl+c").priority is True
|
||||
assert pilot.app._bindings.get_key("tab").priority is False
|
||||
assert pilot.app._bindings.get_key("shift+tab").priority is False
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -61,7 +63,9 @@ class AlphaBinding(App[None]):
|
||||
async def test_just_app_alpha_binding() -> None:
|
||||
"""An app with a single binding should have just the one binding."""
|
||||
async with AlphaBinding().run_test() as pilot:
|
||||
assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"])
|
||||
assert sorted(pilot.app._bindings.keys.keys()) == sorted(
|
||||
["ctrl+c", "tab", "shift+tab", "a"]
|
||||
)
|
||||
assert pilot.app._bindings.get_key("ctrl+c").priority is True
|
||||
assert pilot.app._bindings.get_key("a").priority is True
|
||||
|
||||
@@ -84,7 +88,9 @@ class LowAlphaBinding(App[None]):
|
||||
async def test_just_app_low_priority_alpha_binding() -> None:
|
||||
"""An app with a single low-priority binding should have just the one binding."""
|
||||
async with LowAlphaBinding().run_test() as pilot:
|
||||
assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"])
|
||||
assert sorted(pilot.app._bindings.keys.keys()) == sorted(
|
||||
["ctrl+c", "tab", "shift+tab", "a"]
|
||||
)
|
||||
assert pilot.app._bindings.get_key("ctrl+c").priority is True
|
||||
assert pilot.app._bindings.get_key("a").priority is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user