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
|
||||||
|
|
||||||
- Fixed issues with nested auto dimensions https://github.com/Textualize/textual/issues/1402
|
- 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
|
||||||
|
|
||||||
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings.
|
- 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
|
## [0.7.0] - 2022-12-17
|
||||||
|
|
||||||
|
|||||||
@@ -240,6 +240,14 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
TITLE: str | None = None
|
TITLE: str | None = None
|
||||||
SUB_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("")
|
title: Reactive[str] = Reactive("")
|
||||||
sub_title: Reactive[str] = Reactive("")
|
sub_title: Reactive[str] = Reactive("")
|
||||||
dark: Reactive[bool] = Reactive(True)
|
dark: Reactive[bool] = Reactive(True)
|
||||||
@@ -301,7 +309,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
self._logger = Logger(self._log)
|
self._logger = Logger(self._log)
|
||||||
|
|
||||||
self._bindings.bind("ctrl+c", "quit", show=False, priority=True)
|
|
||||||
self._refresh_required = False
|
self._refresh_required = False
|
||||||
|
|
||||||
self.design = DEFAULT_COLORS
|
self.design = DEFAULT_COLORS
|
||||||
@@ -1898,11 +1905,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
message.stop()
|
message.stop()
|
||||||
|
|
||||||
async def _on_key(self, event: events.Key) -> None:
|
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)):
|
if not (await self.check_bindings(event.key)):
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
@@ -2124,6 +2126,14 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
async def action_toggle_class(self, selector: str, class_name: str) -> None:
|
async def action_toggle_class(self, selector: str, class_name: str) -> None:
|
||||||
self.screen.query(selector).toggle_class(class_name)
|
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(
|
def _on_terminal_supports_synchronized_output(
|
||||||
self, message: messages.TerminalSupportsSynchronizedOutput
|
self, message: messages.TerminalSupportsSynchronizedOutput
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -123,3 +123,11 @@ def colors():
|
|||||||
from textual.cli.previews import colors
|
from textual.cli.previews import colors
|
||||||
|
|
||||||
colors.app.run()
|
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 ._context import NoActiveAppError
|
||||||
from ._node_list import NodeList
|
from ._node_list import NodeList
|
||||||
from .binding import Binding, Bindings, BindingType
|
from .binding import Bindings, BindingType
|
||||||
from .color import BLACK, WHITE, Color
|
from .color import BLACK, WHITE, Color
|
||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
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
|
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:
|
def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
@@ -209,7 +209,9 @@ class Key(InputEvent):
|
|||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield "key", self.key
|
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
|
@property
|
||||||
def key_name(self) -> str | None:
|
def key_name(self) -> str | None:
|
||||||
|
|||||||
@@ -61,11 +61,18 @@ class TextLog(ScrollView, can_focus=True):
|
|||||||
def _on_styles_updated(self) -> None:
|
def _on_styles_updated(self) -> None:
|
||||||
self._line_cache.clear()
|
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.
|
"""Write text or a rich renderable.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content (RenderableType): Rich renderable (or text).
|
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
|
renderable: RenderableType
|
||||||
@@ -88,13 +95,17 @@ class TextLog(ScrollView, can_focus=True):
|
|||||||
if isinstance(renderable, Text) and not self.wrap:
|
if isinstance(renderable, Text) and not self.wrap:
|
||||||
render_options = render_options.update(overflow="ignore", no_wrap=True)
|
render_options = render_options.update(overflow="ignore", no_wrap=True)
|
||||||
|
|
||||||
width = max(
|
if expand:
|
||||||
self.min_width,
|
render_width = self.scrollable_content_region.width
|
||||||
measure_renderables(console, render_options, [renderable]).maximum,
|
else:
|
||||||
|
render_width = (
|
||||||
|
measure_renderables(console, render_options, [renderable]).maximum
|
||||||
|
if width is None
|
||||||
|
else width
|
||||||
)
|
)
|
||||||
|
|
||||||
segments = self.app.console.render(
|
segments = self.app.console.render(
|
||||||
renderable, render_options.update_width(width)
|
renderable, render_options.update_width(render_width)
|
||||||
)
|
)
|
||||||
lines = list(Segment.split_lines(segments))
|
lines = list(Segment.split_lines(segments))
|
||||||
if not lines:
|
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:
|
async def test_just_app_no_bindings() -> None:
|
||||||
"""An app with no bindings should have no bindings, other than ctrl+c."""
|
"""An app with no bindings should have no bindings, other than ctrl+c."""
|
||||||
async with NoBindings().run_test() as pilot:
|
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("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:
|
async def test_just_app_alpha_binding() -> None:
|
||||||
"""An app with a single binding should have just the one binding."""
|
"""An app with a single binding should have just the one binding."""
|
||||||
async with AlphaBinding().run_test() as pilot:
|
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("ctrl+c").priority is True
|
||||||
assert pilot.app._bindings.get_key("a").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:
|
async def test_just_app_low_priority_alpha_binding() -> None:
|
||||||
"""An app with a single low-priority binding should have just the one binding."""
|
"""An app with a single low-priority binding should have just the one binding."""
|
||||||
async with LowAlphaBinding().run_test() as pilot:
|
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("ctrl+c").priority is True
|
||||||
assert pilot.app._bindings.get_key("a").priority is False
|
assert pilot.app._bindings.get_key("a").priority is False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user