diff --git a/CHANGELOG.md b/CHANGELOG.md index e550842bba..326ba48e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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.42.2] - 2023-11-29 + +### Fixed + +- Fixed NoWidget error https://github.com/Textualize/textual/pull/3779 + ## [0.43.1] - 2023-11-29 ### Fixed @@ -1465,6 +1471,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.43.2]: https://github.com/Textualize/textual/compare/v0.43.1...v0.43.2 [0.43.1]: https://github.com/Textualize/textual/compare/v0.43.0...v0.43.1 [0.43.0]: https://github.com/Textualize/textual/compare/v0.42.0...v0.43.0 [0.42.0]: https://github.com/Textualize/textual/compare/v0.41.0...v0.42.0 diff --git a/pyproject.toml b/pyproject.toml index b95f190d2d..2087d691e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.43.1" +version = "0.43.2" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" diff --git a/src/textual/app.py b/src/textual/app.py index e76b96a847..108dd4daba 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -79,6 +79,7 @@ from .dom import DOMNode from .driver import Driver from .drivers.headless_driver import HeadlessDriver +from .errors import NoWidget from .features import FeatureFlag, parse_features from .file_monitor import FileMonitor from .geometry import Offset, Region, Size @@ -444,6 +445,9 @@ def __init__( self._animate = self._animator.bind(self) self.mouse_position = Offset(0, 0) + self._mouse_down_widget: Widget | None = None + """The widget that was most recently mouse downed (used to create click events).""" + self.cursor_position = Offset(0, 0) """The position of the terminal cursor in screen-space. @@ -2671,7 +2675,7 @@ async def on_event(self, event: events.Event) -> None: # Handle input events that haven't been forwarded # If the event has been forwarded it may have bubbled up back to the App if isinstance(event, events.Compose): - screen = Screen(id=f"_default") + screen: Screen[Any] = Screen(id=f"_default") self._register(self, screen) self._screen_stack.append(screen) screen.post_message(events.ScreenResume()) @@ -2683,7 +2687,26 @@ async def on_event(self, event: events.Event) -> None: if isinstance(event, events.MouseEvent): # Record current mouse position on App self.mouse_position = Offset(event.x, event.y) + + if isinstance(event, events.MouseDown): + try: + self._mouse_down_widget, _ = self.get_widget_at( + event.x, event.y + ) + except NoWidget: + # Shouldn't occur, since at the very least this will find the Screen + self._mouse_down_widget = None + self.screen._forward_event(event) + + if isinstance(event, events.MouseUp): + if self._mouse_down_widget is not None and ( + self.get_widget_at(event.x, event.y)[0] + is self._mouse_down_widget + ): + click_event = events.Click.from_event(event) + self.screen._forward_event(click_event) + elif isinstance(event, events.Key): if not await self.check_bindings(event.key, priority=True): forward_target = self.focused or self.screen diff --git a/src/textual/driver.py b/src/textual/driver.py index 5e472f5bc6..7cada2a473 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -9,7 +9,6 @@ if TYPE_CHECKING: from .app import App - from .widget import Widget class Driver(ABC): @@ -33,7 +32,6 @@ def __init__( self._debug = debug self._size = size self._loop = asyncio.get_running_loop() - self._mouse_down_widget: Widget | None = None self._down_buttons: list[int] = [] self._last_move_event: events.MouseMove | None = None @@ -58,17 +56,15 @@ def process_event(self, event: events.Event) -> None: Args: event: An event to send. """ + # NOTE: This runs in a thread. + # Avoid calling methods on the app. event._set_sender(self._app) if isinstance(event, events.MouseDown): - self._mouse_down_widget = self._app.get_widget_at(event.x, event.y)[0] if event.button: self._down_buttons.append(event.button) elif isinstance(event, events.MouseUp): - if event.button: - try: - self._down_buttons.remove(event.button) - except ValueError: - pass + if event.button and event.button in self._down_buttons: + self._down_buttons.remove(event.button) elif isinstance(event, events.MouseMove): if ( self._down_buttons @@ -99,13 +95,6 @@ def process_event(self, event: events.Event) -> None: self.send_event(event) - if ( - isinstance(event, events.MouseUp) - and self._app.get_widget_at(event.x, event.y)[0] is self._mouse_down_widget - ): - click_event = events.Click.from_event(event) - self.send_event(click_event) - @abstractmethod def write(self, data: str) -> None: """Write data to the output device. diff --git a/src/textual/pilot.py b/src/textual/pilot.py index c3bcbe2978..07873ab36f 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -323,12 +323,12 @@ async def _post_mouse_events( # Get the widget under the mouse before the event because the app might # react to the event and move things around. We override on each iteration # because we assume the final event in `events` is the actual event we care - # about and that all the preceeding events are just setup. - # E.g., the click event is preceeded by MouseDown/MouseUp to emulate how + # about and that all the preceding events are just setup. + # E.g., the click event is preceded by MouseDown/MouseUp to emulate how # the driver works and emits a click event. widget_at, _ = app.get_widget_at(*offset) event = mouse_event_cls(**message_arguments) - app.post_message(event) + app.screen._forward_event(event) await self.pause() return selector is None or widget_at is target_widget diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 84857386ae..a1da279f1c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -8002,136 +8002,136 @@ font-weight: 700; } - .terminal-3953145704-matrix { + .terminal-3762053931-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3953145704-title { + .terminal-3762053931-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3953145704-r1 { fill: #c5c8c6 } - .terminal-3953145704-r2 { fill: #008000 } - .terminal-3953145704-r3 { fill: #e8e0e7 } - .terminal-3953145704-r4 { fill: #eae3e5 } - .terminal-3953145704-r5 { fill: #ede6e6 } - .terminal-3953145704-r6 { fill: #efe9e4 } - .terminal-3953145704-r7 { fill: #efeedf } + .terminal-3762053931-r1 { fill: #c5c8c6 } + .terminal-3762053931-r2 { fill: #008000 } + .terminal-3762053931-r3 { fill: #e8e0e7 } + .terminal-3762053931-r4 { fill: #eae3e5 } + .terminal-3762053931-r5 { fill: #1e1e1e } + .terminal-3762053931-r6 { fill: #ede6e6 } + .terminal-3762053931-r7 { fill: #efeedf } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - KeylineApp + KeylineApp - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - #foo - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#bar - - - PlaceholderPlaceholder - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - #baz - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + #foo + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#bar + + + Placeholder + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + #baz + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +