Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Action pump #4657

Merged
merged 9 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## [0.69.0] - 2024-06-16

### Added

- Added `App.simulate_key` https://github.com/Textualize/textual/pull/4657

### Fixed

- Fixed issue with pop_screen launched from an action https://github.com/Textualize/textual/pull/4657

### Changed

- `App.check_bindings` is now private
- `App.action_check_bindings` is now `App.action_simulate_key`

## [0.68.0] - 2024-06-14

### Added
Expand Down Expand Up @@ -2132,6 +2148,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[0.69.0]: https://github.com/Textualize/textual/compare/v0.68.0...v0.69.0
[0.68.0]: https://github.com/Textualize/textual/compare/v0.67.1...v0.68.0
[0.67.1]: https://github.com/Textualize/textual/compare/v0.67.0...v0.67.1
[0.67.0]: https://github.com/Textualize/textual/compare/v0.66.0...v0.67.0
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,16 @@ Textual supports the following builtin actions which are defined on the app.
- [action_add_class][textual.app.App.action_add_class]
- [action_back][textual.app.App.action_back]
- [action_bell][textual.app.App.action_bell]
- [action_check_bindings][textual.app.App.action_check_bindings]
- [action_focus][textual.app.App.action_focus]
- [action_focus_next][textual.app.App.action_focus_next]
- [action_focus_previous][textual.app.App.action_focus_previous]
- [action_focus][textual.app.App.action_focus]
- [action_pop_screen][textual.app.App.action_pop_screen]
- [action_push_screen][textual.app.App.action_push_screen]
- [action_quit][textual.app.App.action_quit]
- [action_remove_class][textual.app.App.action_remove_class]
- [action_screenshot][textual.app.App.action_screenshot]
- [action_switch_screen][textual.app.App.action_switch_screen]
- [action_simulate_key][textual.app.App.action_simulate_key]
- [action_suspend_process][textual.app.App.action_suspend_process]
- [action_switch_screen][textual.app.App.action_switch_screen]
- [action_toggle_class][textual.app.App.action_toggle_class]
- [action_toggle_dark][textual.app.App.action_toggle_dark]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.68.0"
version = "0.69.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
31 changes: 21 additions & 10 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2977,11 +2977,20 @@ def _binding_chain(self) -> list[tuple[DOMNode, _Bindings]]:

return namespace_bindings

async def check_bindings(self, key: str, priority: bool = False) -> bool:
def simulate_key(self, key: str) -> None:
"""Simulate a key press.

This will perform the same action as if the user had pressed the key.

Args:
key: Key to simulate. May also be the name of a key, e.g. "space".
"""
self.call_later(self._check_bindings, key)

async def _check_bindings(self, key: str, priority: bool = False) -> bool:
"""Handle a key press.

This method is used internally by the bindings system, but may be called directly
if you wish to *simulate* a key being pressed.
This method is used internally by the bindings system.

Args:
key: A key.
Expand Down Expand Up @@ -3049,7 +3058,7 @@ async def on_event(self, event: events.Event) -> None:
self.screen._clear_tooltip()
except NoScreen:
pass
if not await self.check_bindings(event.key, priority=True):
if not await self._check_bindings(event.key, priority=True):
forward_target = self.focused or self.screen
forward_target._forward_event(event)
else:
Expand Down Expand Up @@ -3138,7 +3147,7 @@ async def run_action(
return False

async def _dispatch_action(
self, namespace: object, action_name: str, params: Any
self, namespace: DOMNode, action_name: str, params: Any
) -> bool:
"""Dispatch an action to an action method.

Expand Down Expand Up @@ -3175,6 +3184,7 @@ async def _dispatch_action(
except SkipAction:
# The action method raised this to explicitly not handle the action
log.system(f"<action> {action_name!r} skipped.")

return False

async def _broker_event(
Expand Down Expand Up @@ -3230,7 +3240,7 @@ async def _on_layout(self, message: messages.Layout) -> None:
message.stop()

async def _on_key(self, event: events.Key) -> None:
if not (await self.check_bindings(event.key)):
if not (await self._check_bindings(event.key)):
await self.dispatch_key(event)

async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
Expand Down Expand Up @@ -3461,14 +3471,15 @@ def _watch_app_focus(self, focus: bool) -> None:
# Remove focus for now.
self.screen.set_focus(None)

async def action_check_bindings(self, key: str) -> None:
"""An [action](/guide/actions) to handle a key press using the binding system.
async def action_simulate_key(self, key: str) -> None:
"""An [action](/guide/actions) to simulate a key press.

This will invoke the same actions as if the user had pressed the key.

Args:
key: The key to process.
"""
if not await self.check_bindings(key, priority=True):
await self.check_bindings(key, priority=False)
self.simulate_key(key)

async def action_quit(self) -> None:
"""An [action](/guide/actions) to quit the app as soon as possible."""
Expand Down
1 change: 1 addition & 0 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"""Type of a screen result callback function."""


@rich.repr.auto
class ResultCallback(Generic[ScreenResultType]):
"""Holds the details of a callback."""

Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_classic_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _make_key_text(self) -> Text:
),
meta=(
{
"@click": f"app.check_bindings('{binding.key}')",
"@click": f"app.simulate_key('{binding.key}')",
"key": binding.key,
}
if enabled and app_focus
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async def on_mouse_down(self) -> None:
if self._disabled:
self.app.bell()
else:
await self.app.check_bindings(self.key)
self.app.simulate_key(self.key)

def _watch_compact(self, compact: bool) -> None:
self.set_class(compact, "-compact")
Expand Down
94 changes: 94 additions & 0 deletions tests/test_modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Header, Label

TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class QuitScreen(ModalScreen[bool]): # (1)!
"""Screen with a dialog to quit."""

def compose(self) -> ComposeResult:
yield Grid(
Label("Are you sure you want to quit?", id="question"),
Button("Quit", variant="error", id="quit"),
Button("Cancel", variant="primary", id="cancel"),
id="dialog",
)

def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "quit":
self.dismiss(True)
else:
self.dismiss(False)


class ModalApp(App):
"""An app with a modal dialog."""

BINDINGS = [("q", "request_quit", "Quit")]

CSS = """
QuitScreen {
align: center middle;
}

#dialog {
grid-size: 2;
grid-gutter: 1 2;
grid-rows: 1fr 3;
padding: 0 1;
width: 60;
height: 11;
border: thick $background 80%;
background: $surface;
}

#question {
column-span: 2;
height: 1fr;
width: 1fr;
content-align: center middle;
}

Button {
width: 100%;
}

"""

def compose(self) -> ComposeResult:
yield Header()
yield Label(TEXT * 8)
yield Footer()

def action_request_quit(self) -> None:
"""Action to display the quit dialog."""

def check_quit(quit: bool) -> None:
"""Called when QuitScreen is dismissed."""

if quit:
self.exit()

self.push_screen(QuitScreen(), check_quit)


async def test_modal_pop_screen():
# https://github.com/Textualize/textual/issues/4656

async with ModalApp().run_test() as pilot:
await pilot.pause()
# Check clicking the footer brings up the quit screen
await pilot.click(Footer)
assert isinstance(pilot.app.screen, QuitScreen)
# Check activating the quit button exits the app
await pilot.press("enter")
assert pilot.app._exit
Loading