Command palette events (#4600)

* Add some events for the command palette

* Sending open, close, and highlighted events from CommandPalette -> App

* Docstrings

* Changelog

* Use Union for old python support

* Docstrings
This commit is contained in:
Darren Burns
2024-06-05 11:47:59 +01:00
committed by GitHub
parent 4db117c3c5
commit 7b0251ee04
3 changed files with 104 additions and 0 deletions

View File

@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
### Added
- Added Command Palette Opened, Closed, and OptionHighlighted events https://github.com/Textualize/textual/pull/4600
### Fixed
- Fixed DataTable cursor flicker on scroll https://github.com/Textualize/textual/pull/4598

View File

@@ -34,6 +34,7 @@ from .binding import Binding, BindingType
from .containers import Horizontal, Vertical
from .events import Click, Mount
from .fuzzy import Matcher
from .message import Message
from .reactive import var
from .screen import Screen, _SystemModalScreen
from .timer import Timer
@@ -541,6 +542,24 @@ class CommandPalette(_SystemModalScreen[CallbackType]):
_PALETTE_ID: Final[str] = "--command-palette"
"""The internal ID for the command palette."""
@dataclass
class OptionHighlighted(Message):
"""Posted to App when an option is highlighted in the command palette."""
highlighted_event: OptionList.OptionHighlighted
"""The option highlighted event from the OptionList within the command palette."""
@dataclass
class Opened(Message):
"""Posted to App when the command palette is opened."""
@dataclass
class Closed(Message):
"""Posted to App when the command palette is closed."""
option_selected: bool
"""True if an option was selected, False if the palette was closed without selecting an option."""
def __init__(self) -> None:
"""Initialise the command palette."""
super().__init__(id=self._PALETTE_ID)
@@ -623,10 +642,13 @@ class CommandPalette(_SystemModalScreen[CallbackType]):
"""
if self.get_widget_at(event.screen_x, event.screen_y)[0] is self:
self._cancel_gather_commands()
self.app.post_message(CommandPalette.Closed(option_selected=False))
self.dismiss()
def _on_mount(self, _: Mount) -> None:
"""Configure the command palette once the DOM is ready."""
self.app.post_message(CommandPalette.Opened())
self._calling_screen = self.app.screen_stack[-2]
match_style = self.get_component_rich_style(
@@ -1085,12 +1107,14 @@ class CommandPalette(_SystemModalScreen[CallbackType]):
# ...we should return it to the parent screen and let it
# decide what to do with it (hopefully it'll run it).
self._cancel_gather_commands()
self.app.post_message(CommandPalette.Closed(option_selected=True))
self.dismiss(self._selected_command.command)
@on(OptionList.OptionHighlighted)
def _stop_event_leak(self, event: OptionList.OptionHighlighted) -> None:
"""Stop any unused events so they don't leak to the application."""
event.stop()
self.app.post_message(CommandPalette.OptionHighlighted(highlighted_event=event))
def _action_escape(self) -> None:
"""Handle a request to escape out of the command palette."""
@@ -1098,6 +1122,7 @@ class CommandPalette(_SystemModalScreen[CallbackType]):
self._list_visible = False
else:
self._cancel_gather_commands()
self.app.post_message(CommandPalette.Closed(option_selected=False))
self.dismiss()
def _action_command_list(self, action: str) -> None:

View File

@@ -0,0 +1,75 @@
from typing import Union
from unittest import mock
from textual import on
from textual.app import App
from textual.command import CommandPalette, Hit, Hits, Provider
CommandPaletteEvent = Union[
CommandPalette.Opened, CommandPalette.Closed, CommandPalette.OptionHighlighted
]
class SimpleSource(Provider):
async def search(self, query: str) -> Hits:
def goes_nowhere_does_nothing() -> None:
pass
yield Hit(1, query, goes_nowhere_does_nothing, query)
class AppWithActiveCommandPalette(App[None]):
COMMANDS = {SimpleSource}
def __init__(self) -> None:
super().__init__()
self.events: list[CommandPaletteEvent] = []
def on_mount(self) -> None:
self.action_command_palette()
@on(CommandPalette.Opened)
@on(CommandPalette.Closed)
@on(CommandPalette.OptionHighlighted)
def record_event(
self,
event: CommandPaletteEvent,
) -> None:
self.events.append(event)
async def test_command_palette_opened_event():
app = AppWithActiveCommandPalette()
async with app.run_test():
assert app.events == [CommandPalette.Opened()]
async def test_command_palette_closed_event():
app = AppWithActiveCommandPalette()
async with app.run_test() as pilot:
await pilot.press("escape")
assert app.events == [CommandPalette.Opened(), CommandPalette.Closed(False)]
async def test_command_palette_closed_event_value():
app = AppWithActiveCommandPalette()
async with app.run_test() as pilot:
await pilot.press("a")
await pilot.press("down")
await pilot.press("enter")
assert app.events == [
CommandPalette.Opened(),
CommandPalette.OptionHighlighted(mock.ANY),
CommandPalette.Closed(True),
]
async def test_command_palette_option_highlighted_event():
app = AppWithActiveCommandPalette()
async with app.run_test() as pilot:
await pilot.press("a")
await pilot.press("down")
assert app.events == [
CommandPalette.Opened(),
CommandPalette.OptionHighlighted(mock.ANY),
]