From e13ea5e0bd98a9f938b23abdab6bdc61f9283409 Mon Sep 17 00:00:00 2001 From: Nitzan Shaked Date: Fri, 30 Dec 2022 18:44:09 +0200 Subject: [PATCH 1/7] MouseScrollUp/MouseScrollDown => plain MousEvent's ... which means they get passesd x, y, etc. In particular, they are passed the keyboard modifiers. This allows widgets to use e.g. ctrl-wheel to scroll right/left. --- src/textual/_xterm_parser.py | 47 ++++++++++++++++++------------------ src/textual/events.py | 20 +++++---------- tests/test_xterm_parser.py | 24 ++++++++++-------- 3 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 40af686a4..d80489e84 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -54,36 +54,35 @@ class XTermParser(Parser[events.Event]): if sgr_match: _buttons, _x, _y, state = sgr_match.groups() buttons = int(_buttons) - button = (buttons + 1) & 3 x = int(_x) - 1 y = int(_y) - 1 delta_x = x - self.last_x delta_y = y - self.last_y self.last_x = x self.last_y = y - event: events.Event - if buttons & 64: - event = ( - events.MouseScrollUp if button == 1 else events.MouseScrollDown - )(sender, x, y) - else: - event = ( - events.MouseMove - if buttons & 32 - else (events.MouseDown if state == "M" else events.MouseUp) - )( - sender, - x, - y, - delta_x, - delta_y, - button, - bool(buttons & 4), - bool(buttons & 8), - bool(buttons & 16), - screen_x=x, - screen_y=y, - ) + event_cls = ( + (events.MouseScrollDown if buttons & 1 else events.MouseScrollUp) + if buttons & 64 + else events.MouseMove + if buttons & 32 + else (events.MouseDown if state == "M" else events.MouseUp) + ) + if event_cls in (events.MouseScrollUp, events.MouseScrollDown): + buttons &= ~(64 | 3) + button = (buttons + 1) & 3 + event = event_cls( + sender, + x, + y, + delta_x, + delta_y, + button, + bool(buttons & 4), + bool(buttons & 8), + bool(buttons & 16), + screen_x=x, + screen_y=y, + ) return event return None diff --git a/src/textual/events.py b/src/textual/events.py index 1f509a072..e99007e21 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -419,22 +419,14 @@ class MouseUp(MouseEvent, bubble=True, verbose=True): pass -class MouseScrollDown(InputEvent, bubble=True, verbose=True): - __slots__ = ["x", "y"] - - def __init__(self, sender: MessageTarget, x: int, y: int) -> None: - super().__init__(sender) - self.x = x - self.y = y +@rich.repr.auto +class MouseScrollDown(MouseEvent, bubble=True): + pass -class MouseScrollUp(InputEvent, bubble=True, verbose=True): - __slots__ = ["x", "y"] - - def __init__(self, sender: MessageTarget, x: int, y: int) -> None: - super().__init__(sender) - self.x = x - self.y = y +@rich.repr.auto +class MouseScrollUp(MouseEvent, bubble=True): + pass class Click(MouseEvent, bubble=True): diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index 6e352d87d..a8a951b02 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -236,14 +236,14 @@ def test_mouse_move(parser, sequence, shift, meta, button): @pytest.mark.parametrize( - "sequence", + "sequence, shift, meta", [ - "\x1b[<64;18;25M", - "\x1b[<68;18;25M", - "\x1b[<72;18;25M", + ("\x1b[<64;18;25M", False, False), + ("\x1b[<68;18;25M", True, False), + ("\x1b[<72;18;25M", False, True), ], ) -def test_mouse_scroll_up(parser, sequence): +def test_mouse_scroll_up(parser, sequence, shift, meta): """Scrolling the mouse with and without modifiers held down. We don't currently capture modifier keys in scroll events. """ @@ -256,17 +256,19 @@ def test_mouse_scroll_up(parser, sequence): assert isinstance(event, MouseScrollUp) assert event.x == 17 assert event.y == 24 + assert event.shift is shift + assert event.meta is meta @pytest.mark.parametrize( - "sequence", + "sequence, shift, meta", [ - "\x1b[<65;18;25M", - "\x1b[<69;18;25M", - "\x1b[<73;18;25M", + ("\x1b[<65;18;25M", False, False), + ("\x1b[<69;18;25M", True, False), + ("\x1b[<73;18;25M", False, True), ], ) -def test_mouse_scroll_down(parser, sequence): +def test_mouse_scroll_down(parser, sequence, shift, meta): events = list(parser.feed(sequence)) assert len(events) == 1 @@ -276,6 +278,8 @@ def test_mouse_scroll_down(parser, sequence): assert isinstance(event, MouseScrollDown) assert event.x == 17 assert event.y == 24 + assert event.shift is shift + assert event.meta is meta def test_mouse_event_detected_but_info_not_parsed(parser): From 0b249bdca2bf70a7d5562263c3d86128d3d50025 Mon Sep 17 00:00:00 2001 From: Nitzan Shaked Date: Mon, 2 Jan 2023 21:19:20 +0200 Subject: [PATCH 2/7] fix PR comments --- src/textual/_xterm_parser.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index d80489e84..03ac17ced 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -60,16 +60,19 @@ class XTermParser(Parser[events.Event]): delta_y = y - self.last_y self.last_x = x self.last_y = y - event_cls = ( - (events.MouseScrollDown if buttons & 1 else events.MouseScrollUp) - if buttons & 64 - else events.MouseMove - if buttons & 32 - else (events.MouseDown if state == "M" else events.MouseUp) - ) - if event_cls in (events.MouseScrollUp, events.MouseScrollDown): - buttons &= ~(64 | 3) - button = (buttons + 1) & 3 + + event_cls: type[events.MouseEvent] + button: int + if buttons & 64: + event_cls = events.MouseScrollDown if buttons & 1 else events.MouseScrollUp + button = 0 + else: + event_cls = ( + events.MouseMove + if buttons & 32 + else (events.MouseDown if state == "M" else events.MouseUp) + ) + button = (buttons + 1) & 3 event = event_cls( sender, x, From 67d2c8df7db3da389a71d9c8fdb23833ae244cec Mon Sep 17 00:00:00 2001 From: Nitzan Shaked Date: Mon, 2 Jan 2023 21:59:33 +0200 Subject: [PATCH 3/7] black --- src/textual/_xterm_parser.py | 4 +++- tests/test_xterm_parser.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 03ac17ced..3da9c5f5b 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -64,7 +64,9 @@ class XTermParser(Parser[events.Event]): event_cls: type[events.MouseEvent] button: int if buttons & 64: - event_cls = events.MouseScrollDown if buttons & 1 else events.MouseScrollUp + event_cls = ( + events.MouseScrollDown if buttons & 1 else events.MouseScrollUp + ) button = 0 else: event_cls = ( diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index a8a951b02..1b426267b 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -239,7 +239,7 @@ def test_mouse_move(parser, sequence, shift, meta, button): "sequence, shift, meta", [ ("\x1b[<64;18;25M", False, False), - ("\x1b[<68;18;25M", True, False), + ("\x1b[<68;18;25M", True, False), ("\x1b[<72;18;25M", False, True), ], ) @@ -264,7 +264,7 @@ def test_mouse_scroll_up(parser, sequence, shift, meta): "sequence, shift, meta", [ ("\x1b[<65;18;25M", False, False), - ("\x1b[<69;18;25M", True, False), + ("\x1b[<69;18;25M", True, False), ("\x1b[<73;18;25M", False, True), ], ) From fa0780246816b16ca890aabdfb2cb23d34a3d985 Mon Sep 17 00:00:00 2001 From: Andrii Yurchuk Date: Wed, 4 Jan 2023 11:42:38 +0100 Subject: [PATCH 4/7] Fix typo --- docs/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index e618be668..7d37eea58 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -365,7 +365,7 @@ The first argument to `reactive` may be a default value or a callable that retur The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start. -The `on_mount` method is an event handler called then the widget is first added to the application (or _mounted_). In this method we call [set_interval()][textual.message_pump.MessagePump.set_interval] to create a timer which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers. +The `on_mount` method is an event handler called when the widget is first added to the application (or _mounted_). In this method we call [set_interval()][textual.message_pump.MessagePump.set_interval] to create a timer which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers. If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified. From d5c8516ee742dc49c0845a5220ee865403d93e84 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 Jan 2023 11:30:20 +0000 Subject: [PATCH 5/7] refactor for readability --- src/textual/_xterm_parser.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 3da9c5f5b..ab760e242 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -60,22 +60,22 @@ class XTermParser(Parser[events.Event]): delta_y = y - self.last_y self.last_x = x self.last_y = y + event_class: type[events.MouseEvent] - event_cls: type[events.MouseEvent] - button: int if buttons & 64: - event_cls = ( + event_class = ( events.MouseScrollDown if buttons & 1 else events.MouseScrollUp ) button = 0 else: - event_cls = ( - events.MouseMove - if buttons & 32 - else (events.MouseDown if state == "M" else events.MouseUp) - ) + if buttons & 32: + event_class = events.MouseMove + else: + event_class = events.MouseDown if state == "M" else events.MouseUp + button = (buttons + 1) & 3 - event = event_cls( + + event = event_class( sender, x, y, From 1465063b71f0646773f83e4663de230daa97a772 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 Jan 2023 11:33:16 +0000 Subject: [PATCH 6/7] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee7b9a67..876edeb4b 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.10.0] - Unreleased + +### Changed + +- `MouseScrollUp` and `MouseScrollDown` now inherit from `MouseEvent` and have attached modifier keys. https://github.com/Textualize/textual/pull/1458 + ## [0.9.1] - 2022-12-30 ### Added From 9a4d61ac603fa560c31666e654f6d9e3f640dd50 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 Jan 2023 11:34:07 +0000 Subject: [PATCH 7/7] black --- src/textual/_win_sleep.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index 4d417a07e..e90930afc 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -12,7 +12,6 @@ WAIT_FAILED = 0xFFFFFFFF CREATE_WAITABLE_TIMER_HIGH_RESOLUTION = 0x00000002 - def sleep(sleep_for: float) -> None: """A replacement sleep for Windows.