Skip to content

Commit b7471e4

Browse files
authored
Merge pull request #4657 from Textualize/action-pump
Action pump
2 parents e9ad400 + 2375ed2 commit b7471e4

File tree

8 files changed

+139
-16
lines changed

8 files changed

+139
-16
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
9+
## [0.69.0] - 2024-06-16
10+
11+
### Added
12+
13+
- Added `App.simulate_key` https://github.com/Textualize/textual/pull/4657
14+
15+
### Fixed
16+
17+
- Fixed issue with pop_screen launched from an action https://github.com/Textualize/textual/pull/4657
18+
19+
### Changed
20+
21+
- `App.check_bindings` is now private
22+
- `App.action_check_bindings` is now `App.action_simulate_key`
23+
824
## [0.68.0] - 2024-06-14
925

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

2151+
[0.69.0]: https://github.com/Textualize/textual/compare/v0.68.0...v0.69.0
21352152
[0.68.0]: https://github.com/Textualize/textual/compare/v0.67.1...v0.68.0
21362153
[0.67.1]: https://github.com/Textualize/textual/compare/v0.67.0...v0.67.1
21372154
[0.67.0]: https://github.com/Textualize/textual/compare/v0.66.0...v0.67.0

docs/guide/actions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,16 +207,16 @@ Textual supports the following builtin actions which are defined on the app.
207207
- [action_add_class][textual.app.App.action_add_class]
208208
- [action_back][textual.app.App.action_back]
209209
- [action_bell][textual.app.App.action_bell]
210-
- [action_check_bindings][textual.app.App.action_check_bindings]
211-
- [action_focus][textual.app.App.action_focus]
212210
- [action_focus_next][textual.app.App.action_focus_next]
213211
- [action_focus_previous][textual.app.App.action_focus_previous]
212+
- [action_focus][textual.app.App.action_focus]
214213
- [action_pop_screen][textual.app.App.action_pop_screen]
215214
- [action_push_screen][textual.app.App.action_push_screen]
216215
- [action_quit][textual.app.App.action_quit]
217216
- [action_remove_class][textual.app.App.action_remove_class]
218217
- [action_screenshot][textual.app.App.action_screenshot]
219-
- [action_switch_screen][textual.app.App.action_switch_screen]
218+
- [action_simulate_key][textual.app.App.action_simulate_key]
220219
- [action_suspend_process][textual.app.App.action_suspend_process]
220+
- [action_switch_screen][textual.app.App.action_switch_screen]
221221
- [action_toggle_class][textual.app.App.action_toggle_class]
222222
- [action_toggle_dark][textual.app.App.action_toggle_dark]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.68.0"
3+
version = "0.69.0"
44
homepage = "https://github.com/Textualize/textual"
55
repository = "https://github.com/Textualize/textual"
66
documentation = "https://textual.textualize.io/"

src/textual/app.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2977,11 +2977,20 @@ def _binding_chain(self) -> list[tuple[DOMNode, _Bindings]]:
29772977

29782978
return namespace_bindings
29792979

2980-
async def check_bindings(self, key: str, priority: bool = False) -> bool:
2980+
def simulate_key(self, key: str) -> None:
2981+
"""Simulate a key press.
2982+
2983+
This will perform the same action as if the user had pressed the key.
2984+
2985+
Args:
2986+
key: Key to simulate. May also be the name of a key, e.g. "space".
2987+
"""
2988+
self.call_later(self._check_bindings, key)
2989+
2990+
async def _check_bindings(self, key: str, priority: bool = False) -> bool:
29812991
"""Handle a key press.
29822992
2983-
This method is used internally by the bindings system, but may be called directly
2984-
if you wish to *simulate* a key being pressed.
2993+
This method is used internally by the bindings system.
29852994
29862995
Args:
29872996
key: A key.
@@ -3049,7 +3058,7 @@ async def on_event(self, event: events.Event) -> None:
30493058
self.screen._clear_tooltip()
30503059
except NoScreen:
30513060
pass
3052-
if not await self.check_bindings(event.key, priority=True):
3061+
if not await self._check_bindings(event.key, priority=True):
30533062
forward_target = self.focused or self.screen
30543063
forward_target._forward_event(event)
30553064
else:
@@ -3138,7 +3147,7 @@ async def run_action(
31383147
return False
31393148

31403149
async def _dispatch_action(
3141-
self, namespace: object, action_name: str, params: Any
3150+
self, namespace: DOMNode, action_name: str, params: Any
31423151
) -> bool:
31433152
"""Dispatch an action to an action method.
31443153
@@ -3175,6 +3184,7 @@ async def _dispatch_action(
31753184
except SkipAction:
31763185
# The action method raised this to explicitly not handle the action
31773186
log.system(f"<action> {action_name!r} skipped.")
3187+
31783188
return False
31793189

31803190
async def _broker_event(
@@ -3230,7 +3240,7 @@ async def _on_layout(self, message: messages.Layout) -> None:
32303240
message.stop()
32313241

32323242
async def _on_key(self, event: events.Key) -> None:
3233-
if not (await self.check_bindings(event.key)):
3243+
if not (await self._check_bindings(event.key)):
32343244
await self.dispatch_key(event)
32353245

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

3464-
async def action_check_bindings(self, key: str) -> None:
3465-
"""An [action](/guide/actions) to handle a key press using the binding system.
3474+
async def action_simulate_key(self, key: str) -> None:
3475+
"""An [action](/guide/actions) to simulate a key press.
3476+
3477+
This will invoke the same actions as if the user had pressed the key.
34663478
34673479
Args:
34683480
key: The key to process.
34693481
"""
3470-
if not await self.check_bindings(key, priority=True):
3471-
await self.check_bindings(key, priority=False)
3482+
self.simulate_key(key)
34723483

34733484
async def action_quit(self) -> None:
34743485
"""An [action](/guide/actions) to quit the app as soon as possible."""

src/textual/screen.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"""Type of a screen result callback function."""
7070

7171

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

src/textual/widgets/_classic_footer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def _make_key_text(self) -> Text:
137137
),
138138
meta=(
139139
{
140-
"@click": f"app.check_bindings('{binding.key}')",
140+
"@click": f"app.simulate_key('{binding.key}')",
141141
"key": binding.key,
142142
}
143143
if enabled and app_focus

src/textual/widgets/_footer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async def on_mouse_down(self) -> None:
103103
if self._disabled:
104104
self.app.bell()
105105
else:
106-
await self.app.check_bindings(self.key)
106+
self.app.simulate_key(self.key)
107107

108108
def _watch_compact(self, compact: bool) -> None:
109109
self.set_class(compact, "-compact")

tests/test_modal.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Grid
3+
from textual.screen import ModalScreen
4+
from textual.widgets import Button, Footer, Header, Label
5+
6+
TEXT = """I must not fear.
7+
Fear is the mind-killer.
8+
Fear is the little-death that brings total obliteration.
9+
I will face my fear.
10+
I will permit it to pass over me and through me.
11+
And when it has gone past, I will turn the inner eye to see its path.
12+
Where the fear has gone there will be nothing. Only I will remain."""
13+
14+
15+
class QuitScreen(ModalScreen[bool]): # (1)!
16+
"""Screen with a dialog to quit."""
17+
18+
def compose(self) -> ComposeResult:
19+
yield Grid(
20+
Label("Are you sure you want to quit?", id="question"),
21+
Button("Quit", variant="error", id="quit"),
22+
Button("Cancel", variant="primary", id="cancel"),
23+
id="dialog",
24+
)
25+
26+
def on_button_pressed(self, event: Button.Pressed) -> None:
27+
if event.button.id == "quit":
28+
self.dismiss(True)
29+
else:
30+
self.dismiss(False)
31+
32+
33+
class ModalApp(App):
34+
"""An app with a modal dialog."""
35+
36+
BINDINGS = [("q", "request_quit", "Quit")]
37+
38+
CSS = """
39+
QuitScreen {
40+
align: center middle;
41+
}
42+
43+
#dialog {
44+
grid-size: 2;
45+
grid-gutter: 1 2;
46+
grid-rows: 1fr 3;
47+
padding: 0 1;
48+
width: 60;
49+
height: 11;
50+
border: thick $background 80%;
51+
background: $surface;
52+
}
53+
54+
#question {
55+
column-span: 2;
56+
height: 1fr;
57+
width: 1fr;
58+
content-align: center middle;
59+
}
60+
61+
Button {
62+
width: 100%;
63+
}
64+
65+
"""
66+
67+
def compose(self) -> ComposeResult:
68+
yield Header()
69+
yield Label(TEXT * 8)
70+
yield Footer()
71+
72+
def action_request_quit(self) -> None:
73+
"""Action to display the quit dialog."""
74+
75+
def check_quit(quit: bool) -> None:
76+
"""Called when QuitScreen is dismissed."""
77+
78+
if quit:
79+
self.exit()
80+
81+
self.push_screen(QuitScreen(), check_quit)
82+
83+
84+
async def test_modal_pop_screen():
85+
# https://github.com/Textualize/textual/issues/4656
86+
87+
async with ModalApp().run_test() as pilot:
88+
await pilot.pause()
89+
# Check clicking the footer brings up the quit screen
90+
await pilot.click(Footer)
91+
assert isinstance(pilot.app.screen, QuitScreen)
92+
# Check activating the quit button exits the app
93+
await pilot.press("enter")
94+
assert pilot.app._exit

0 commit comments

Comments
 (0)