From 8fe360a65de2da3608de28fdd3b7f3a80a6e1fde Mon Sep 17 00:00:00 2001 From: RyanBurgert219 Date: Fri, 5 Mar 2021 18:41:50 -0500 Subject: [PATCH 1/2] Improved mouse support Added more mouse support functionality, including the ability to discriminate which mouse button was pressed as well as reporting click-drags, and mouse movements when no mouse button is pressed. Also improved mouse support while text editing: when you click and drag, your selection will be shown in realtime (as opposed to having to release the mouse press to see the resulting selection). To see this functionality in action, try running examples/full-screen/text-editor.py, write some text, then click and drag over that text to select it. Commit co-authored by: Jonathan Slenders --- prompt_toolkit/input/win32.py | 61 +++-- prompt_toolkit/key_binding/bindings/mouse.py | 231 +++++++++++++++++-- prompt_toolkit/layout/containers.py | 7 +- prompt_toolkit/layout/controls.py | 18 +- prompt_toolkit/layout/scrollable_pane.py | 2 + prompt_toolkit/mouse_events.py | 50 +++- prompt_toolkit/output/vt100.py | 4 + prompt_toolkit/py.typed | 0 prompt_toolkit/widgets/menus.py | 32 ++- 9 files changed, 340 insertions(+), 65 deletions(-) delete mode 100644 prompt_toolkit/py.typed diff --git a/prompt_toolkit/input/win32.py b/prompt_toolkit/input/win32.py index 7f12cdd87d..00f3f3d1ef 100644 --- a/prompt_toolkit/input/win32.py +++ b/prompt_toolkit/input/win32.py @@ -29,7 +29,7 @@ from prompt_toolkit.eventloop.win32 import create_win32_event, wait_for_handles from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys -from prompt_toolkit.mouse_events import MouseEventType +from prompt_toolkit.mouse_events import MouseButton, MouseEventType from prompt_toolkit.win32_types import ( INPUT_RECORD, KEY_EVENT_RECORD, @@ -50,6 +50,13 @@ "detach_win32_input", ] +# Win32 Constants for MOUSE_EVENT_RECORD. +# See: https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +FROM_LEFT_1ST_BUTTON_PRESSED = 0x1 +RIGHTMOST_BUTTON_PRESSED = 0x2 +MOUSE_MOVED = 0x0001 +MOUSE_WHEELED = 0x0004 + class _Win32InputBase(Input): """ @@ -509,42 +516,48 @@ def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> List[KeyPress]: """ Handle mouse events. Return a list of KeyPress instances. """ - FROM_LEFT_1ST_BUTTON_PRESSED = 0x1 - MOUSE_MOVED = 0x0001 - MOUSE_WHEELED = 0x0004 - event_flags = ev.EventFlags button_state = ev.ButtonState - result = [] event_type: Optional[MouseEventType] = None - - # Move events. - if event_flags & MOUSE_MOVED: - if button_state == FROM_LEFT_1ST_BUTTON_PRESSED: - event_type = MouseEventType.MOUSE_DOWN_MOVE + button: MouseButton = MouseButton.NONE # Scroll events. - elif event_flags & MOUSE_WHEELED: + if event_flags & MOUSE_WHEELED: if button_state > 0: event_type = MouseEventType.SCROLL_UP else: event_type = MouseEventType.SCROLL_DOWN + else: + # Handle button state for non-scroll events. + if button_state == FROM_LEFT_1ST_BUTTON_PRESSED: + button = MouseButton.LEFT - # Mouse down (left button). - elif button_state == FROM_LEFT_1ST_BUTTON_PRESSED: - event_type = MouseEventType.MOUSE_DOWN + elif button_state == RIGHTMOST_BUTTON_PRESSED: + button = MouseButton.RIGHT - # No key pressed anymore: mouse up. - else: - event_type = MouseEventType.MOUSE_UP + # Move events. + if event_flags & MOUSE_MOVED: + event_type = MouseEventType.MOUSE_MOVE - if event_type is not None: - data = ";".join( - [event_type.value, str(ev.MousePosition.X), str(ev.MousePosition.Y)] - ) - result.append(KeyPress(Keys.WindowsMouseEvent, data)) - return result + # No key pressed anymore: mouse up. + if event_type is None: + if button_state > 0: + # Some button pressed. + event_type = MouseEventType.MOUSE_DOWN + else: + # No button pressed. + event_type = MouseEventType.MOUSE_UP + + data = ";".join( + [ + button.value, + event_type.value, + str(ev.MousePosition.X), + str(ev.MousePosition.Y), + ] + ) + return [KeyPress(Keys.WindowsMouseEvent, data)] class _Win32Handles: diff --git a/prompt_toolkit/key_binding/bindings/mouse.py b/prompt_toolkit/key_binding/bindings/mouse.py index ed57a6b236..511bd03d4e 100644 --- a/prompt_toolkit/key_binding/bindings/mouse.py +++ b/prompt_toolkit/key_binding/bindings/mouse.py @@ -1,7 +1,14 @@ +from typing import FrozenSet + from prompt_toolkit.data_structures import Point from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent from prompt_toolkit.keys import Keys -from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.mouse_events import ( + MouseButton, + MouseEvent, + MouseEventType, + MouseModifier, +) from prompt_toolkit.utils import is_windows from ..key_bindings import KeyBindings @@ -12,6 +19,163 @@ E = KeyPressEvent +# fmt: off +# flake8: noqa E201 +SCROLL_UP = MouseEventType.SCROLL_UP +SCROLL_DOWN = MouseEventType.SCROLL_DOWN +MOUSE_DOWN = MouseEventType.MOUSE_DOWN +MOUSE_MOVE = MouseEventType.MOUSE_MOVE +MOUSE_UP = MouseEventType.MOUSE_UP + +NO_MODIFIER : FrozenSet[MouseModifier] = frozenset() +SHIFT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT}) +ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT}) +SHIFT_ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT}) +CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL}) +SHIFT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL}) +ALT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL}) +SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL}) +UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset() + +LEFT = MouseButton.LEFT +MIDDLE = MouseButton.MIDDLE +RIGHT = MouseButton.RIGHT +NO_BUTTON = MouseButton.NONE +UNKNOWN_BUTTON = MouseButton.UNKNOWN + +xterm_sgr_mouse_events = { + ( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0 + ( 4, 'm') : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4 + ( 8, 'm') : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8 + (12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12 + (16, 'm') : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16 + (20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20 + (24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24 + (28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28 + + ( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1 + ( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5 + ( 9, 'm') : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9 + (13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13 + (17, 'm') : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17 + (21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21 + (25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25 + (29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29 + + ( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2 + ( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6 + (10, 'm') : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10 + (14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14 + (18, 'm') : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18 + (22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22 + (26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26 + (30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30 + + ( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0 + ( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4 + ( 8, 'M') : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8 + (12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12 + (16, 'M') : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16 + (20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20 + (24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24 + (28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28 + + ( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1 + ( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5 + ( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9 + (13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13 + (17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17 + (21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21 + (25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25 + (29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29 + + ( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2 + ( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6 + (10, 'M') : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10 + (14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14 + (18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18 + (22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22 + (26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26 + (30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30 + + (32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32 + (36, 'M') : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36 + (40, 'M') : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40 + (44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44 + (48, 'M') : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48 + (52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52 + (56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56 + (60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60 + + (33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33 + (37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37 + (41, 'M') : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41 + (45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45 + (49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49 + (53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53 + (57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57 + (61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61 + + (34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34 + (38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38 + (42, 'M') : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42 + (46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46 + (50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50 + (54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54 + (58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58 + (62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62 + + (35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35 + (39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39 + (43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43 + (47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47 + (51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51 + (55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55 + (59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59 + (63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63 + + (64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64 + (68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68 + (72, 'M') : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72 + (76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76 + (80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80 + (84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84 + (88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88 + (92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92 + + (65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65 + (69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69 + (73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73 + (77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77 + (81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81 + (85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85 + (89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89 + (93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93 +} + +typical_mouse_events = { + 32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER), + 33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER), + 34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER), + 35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER), + + 64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER), + 65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER), + 66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER), + 67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER), + + 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), + 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), +} + +urxvt_mouse_events={ + 32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER), + 35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER), + 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), + 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), +} +# fmt:on + def load_mouse_bindings() -> KeyBindings: """ @@ -33,12 +197,11 @@ def _(event: E) -> None: if event.data[2] == "M": # Typical. mouse_event, x, y = map(ord, event.data[3:]) - mouse_event_type = { - 32: MouseEventType.MOUSE_DOWN, - 35: MouseEventType.MOUSE_UP, - 96: MouseEventType.SCROLL_UP, - 97: MouseEventType.SCROLL_DOWN, - }.get(mouse_event) + + # TODO: Is it possible to add modifiers here? + mouse_button, mouse_event_type, mouse_modifier = typical_mouse_events[ + mouse_event + ] # Handle situations where `PosixStdinReader` used surrogateescapes. if x >= 0xDC00: @@ -65,19 +228,24 @@ def _(event: E) -> None: # Parse event type. if sgr: - mouse_event_type = { - (0, "M"): MouseEventType.MOUSE_DOWN, - (0, "m"): MouseEventType.MOUSE_UP, - (64, "M"): MouseEventType.SCROLL_UP, - (65, "M"): MouseEventType.SCROLL_DOWN, - }.get((mouse_event, m)) + try: + ( + mouse_button, + mouse_event_type, + mouse_modifiers, + ) = xterm_sgr_mouse_events[mouse_event, m] + except KeyError: + return + else: - mouse_event_type = { - 32: MouseEventType.MOUSE_DOWN, - 35: MouseEventType.MOUSE_UP, - 96: MouseEventType.SCROLL_UP, - 97: MouseEventType.SCROLL_DOWN, - }.get(mouse_event) + # Some other terminals, like urxvt, Hyper terminal, ... + ( + mouse_button, + mouse_event_type, + mouse_modifiers, + ) = urxvt_mouse_events.get( + mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER) + ) x -= 1 y -= 1 @@ -95,7 +263,14 @@ def _(event: E) -> None: # Call the mouse handler from the renderer. handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - handler(MouseEvent(position=Point(x=x, y=y), event_type=mouse_event_type)) + handler( + MouseEvent( + position=Point(x=x, y=y), + event_type=mouse_event_type, + button=mouse_button, + modifiers=mouse_modifiers, + ) + ) @key_bindings.add(Keys.ScrollUp) def _scroll_up(event: E) -> None: @@ -123,9 +298,10 @@ def _mouse(event: E) -> None: # Parse data. pieces = event.data.split(";") - event_type = MouseEventType(pieces[0]) - x = int(pieces[1]) - y = int(pieces[2]) + button = MouseButton(pieces[0]) + event_type = MouseEventType(pieces[1]) + x = int(pieces[2]) + y = int(pieces[3]) # Make coordinates absolute to the visible part of the terminal. output = event.app.renderer.output @@ -142,6 +318,13 @@ def _mouse(event: E) -> None: # Call the mouse event handler. handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - handler(MouseEvent(position=Point(x=x, y=y), event_type=event_type)) + handler( + MouseEvent( + position=Point(x=x, y=y), + event_type=event_type, + button=button, + modifiers=UNKNOWN_MODIFIER, + ) + ) return key_bindings diff --git a/prompt_toolkit/layout/containers.py b/prompt_toolkit/layout/containers.py index 4b6d2cc8f2..19cc8012ac 100644 --- a/prompt_toolkit/layout/containers.py +++ b/prompt_toolkit/layout/containers.py @@ -1861,6 +1861,8 @@ def mouse_handler(mouse_event: MouseEvent) -> None: MouseEvent( position=Point(x=col, y=row), event_type=mouse_event.event_type, + button=mouse_event.button, + modifiers=mouse_event.modifiers, ) ) break @@ -1871,7 +1873,10 @@ def mouse_handler(mouse_event: MouseEvent) -> None: # Report (0,0) instead.) result = self.content.mouse_handler( MouseEvent( - position=Point(x=0, y=0), event_type=mouse_event.event_type + position=Point(x=0, y=0), + event_type=mouse_event.event_type, + button=mouse_event.button, + modifiers=mouse_event.modifiers, ) ) diff --git a/prompt_toolkit/layout/controls.py b/prompt_toolkit/layout/controls.py index 16e87a81aa..a0c16e1faa 100644 --- a/prompt_toolkit/layout/controls.py +++ b/prompt_toolkit/layout/controls.py @@ -32,7 +32,12 @@ split_lines, ) from prompt_toolkit.lexers import Lexer, SimpleLexer -from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.mouse_events import ( + MouseButton, + MouseEvent, + MouseEventType, + MouseModifier, +) from prompt_toolkit.search import SearchState from prompt_toolkit.selection import SelectionType from prompt_toolkit.utils import get_cwidth @@ -858,8 +863,15 @@ def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": buffer.exit_selection() buffer.cursor_position = index - elif mouse_event.event_type == MouseEventType.MOUSE_DOWN_MOVE: - if buffer.selection_state is None: + elif ( + mouse_event.event_type == MouseEventType.MOUSE_MOVE + and mouse_event.button != MouseButton.NONE + ): + # Click and drag to highlight a selection + if ( + buffer.selection_state is None + and abs(buffer.cursor_position - index) > 0 + ): buffer.start_selection(selection_type=SelectionType.CHARACTERS) buffer.cursor_position = index diff --git a/prompt_toolkit/layout/scrollable_pane.py b/prompt_toolkit/layout/scrollable_pane.py index 4f90b94c1b..a5500d7f7c 100644 --- a/prompt_toolkit/layout/scrollable_pane.py +++ b/prompt_toolkit/layout/scrollable_pane.py @@ -305,6 +305,8 @@ def new_handler(event: MouseEvent) -> None: y=event.position.y + self.vertical_scroll - ypos, ), event_type=event.event_type, + button=event.button, + modifiers=event.modifiers, ) handler(new_event) diff --git a/prompt_toolkit/mouse_events.py b/prompt_toolkit/mouse_events.py index 2e9c7472ed..26f4312043 100644 --- a/prompt_toolkit/mouse_events.py +++ b/prompt_toolkit/mouse_events.py @@ -16,19 +16,48 @@ `UIControl.mouse_handler` is called. """ from enum import Enum +from typing import FrozenSet from .data_structures import Point -__all__ = ["MouseEventType", "MouseEvent"] +__all__ = ["MouseEventType", "MouseButton", "MouseModifier", "MouseEvent"] class MouseEventType(Enum): + # Mouse up: This same event type is fired for all three events: left mouse + # up, right mouse up, or middle mouse up MOUSE_UP = "MOUSE_UP" + + # Mouse down: This implicitly refers to the left mouse down (this event is + # not fired upon pressing the middle or right mouse buttons). MOUSE_DOWN = "MOUSE_DOWN" - MOUSE_DOWN_MOVE = "MOUSE_DOWN_MOVE" + SCROLL_UP = "SCROLL_UP" SCROLL_DOWN = "SCROLL_DOWN" + # Triggered when the left mouse button is held down, and the mouse moves + MOUSE_MOVE = "MOUSE_MOVE" + + +class MouseButton(Enum): + LEFT = "LEFT" + MIDDLE = "MIDDLE" + RIGHT = "RIGHT" + + # When we're scrolling, or just moving the mouse and not pressing a button. + NONE = "NONE" + + # This is for when we don't know which mouse button was pressed, but we do + # know that one has been pressed during this mouse event (as opposed to + # scrolling, for example) + UNKNOWN = "UNKNOWN" + + +class MouseModifier(Enum): + SHIFT = "SHIFT" + ALT = "ALT" + CONTROL = "CONTROL" + class MouseEvent: """ @@ -38,9 +67,22 @@ class MouseEvent: :param event_type: `MouseEventType`. """ - def __init__(self, position: Point, event_type: MouseEventType) -> None: + def __init__( + self, + position: Point, + event_type: MouseEventType, + button: MouseButton, + modifiers: FrozenSet[MouseModifier], + ) -> None: self.position = position self.event_type = event_type + self.button = button + self.modifiers = modifiers def __repr__(self) -> str: - return "MouseEvent(%r, %r)" % (self.position, self.event_type) + return "MouseEvent(%r,%r,%r,%r)" % ( + self.position, + self.event_type, + self.button, + self.modifiers, + ) diff --git a/prompt_toolkit/output/vt100.py b/prompt_toolkit/output/vt100.py index 2645db977e..686303fa7b 100644 --- a/prompt_toolkit/output/vt100.py +++ b/prompt_toolkit/output/vt100.py @@ -551,6 +551,9 @@ def quit_alternate_screen(self) -> None: def enable_mouse_support(self) -> None: self.write_raw("\x1b[?1000h") + # Enable mouse-drag support. + self.write_raw("\x1b[?1003h") + # Enable urxvt Mouse mode. (For terminals that understand this.) self.write_raw("\x1b[?1015h") @@ -564,6 +567,7 @@ def disable_mouse_support(self) -> None: self.write_raw("\x1b[?1000l") self.write_raw("\x1b[?1015l") self.write_raw("\x1b[?1006l") + self.write_raw("\x1b[?1003l") def erase_end_of_line(self) -> None: """ diff --git a/prompt_toolkit/py.typed b/prompt_toolkit/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/prompt_toolkit/widgets/menus.py b/prompt_toolkit/widgets/menus.py index bc4a1dd750..9d53e9d322 100644 --- a/prompt_toolkit/widgets/menus.py +++ b/prompt_toolkit/widgets/menus.py @@ -16,7 +16,7 @@ Window, ) from prompt_toolkit.layout.controls import FormattedTextControl -from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType from prompt_toolkit.utils import get_cwidth from prompt_toolkit.widgets import Shadow @@ -233,14 +233,20 @@ def _get_menu_fragments(self) -> StyleAndTextTuples: # Generate text fragments for the main menu. def one_item(i: int, item: MenuItem) -> Iterable[OneStyleAndTextTuple]: def mouse_handler(mouse_event: MouseEvent) -> None: - if mouse_event.event_type == MouseEventType.MOUSE_UP: + hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE + if ( + mouse_event.event_type == MouseEventType.MOUSE_DOWN + or hover + and focused + ): # Toggle focus. app = get_app() - if app.layout.has_focus(self.window): - if self.selected_menu == [i]: - app.layout.focus_last() - else: - app.layout.focus(self.window) + if not hover: + if app.layout.has_focus(self.window): + if self.selected_menu == [i]: + app.layout.focus_last() + else: + app.layout.focus(self.window) self.selected_menu = [i] yield ("class:menu-bar", " ", mouse_handler) @@ -276,9 +282,17 @@ def one_item( i: int, item: MenuItem ) -> Iterable[OneStyleAndTextTuple]: def mouse_handler(mouse_event: MouseEvent) -> None: - if mouse_event.event_type == MouseEventType.MOUSE_UP: + if item.disabled: + # The arrow keys can't interact with menu items that are disabled. + # The mouse shouldn't be able to either. + return + hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE + if ( + mouse_event.event_type == MouseEventType.MOUSE_UP + or hover + ): app = get_app() - if item.handler: + if not hover and item.handler: app.layout.focus_last() item.handler() else: From 8137774e36e7c8f7ef304201b3262c481a7db1cf Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 20 Aug 2021 11:58:37 +0200 Subject: [PATCH 2/2] Don't repaint if mouse handler returns NotImplemented. --- prompt_toolkit/key_binding/bindings/mouse.py | 30 +++++++++++---- prompt_toolkit/key_binding/key_bindings.py | 40 ++++++++++++++++++-- prompt_toolkit/key_binding/key_processor.py | 8 ---- prompt_toolkit/layout/containers.py | 27 +++++++++---- prompt_toolkit/layout/controls.py | 26 +++---------- prompt_toolkit/layout/menus.py | 6 ++- prompt_toolkit/layout/mouse_handlers.py | 14 ++++--- prompt_toolkit/widgets/menus.py | 2 +- 8 files changed, 98 insertions(+), 55 deletions(-) diff --git a/prompt_toolkit/key_binding/bindings/mouse.py b/prompt_toolkit/key_binding/bindings/mouse.py index 511bd03d4e..949c33f72c 100644 --- a/prompt_toolkit/key_binding/bindings/mouse.py +++ b/prompt_toolkit/key_binding/bindings/mouse.py @@ -1,4 +1,4 @@ -from typing import FrozenSet +from typing import TYPE_CHECKING, FrozenSet from prompt_toolkit.data_structures import Point from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent @@ -13,6 +13,9 @@ from ..key_bindings import KeyBindings +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + __all__ = [ "load_mouse_bindings", ] @@ -185,7 +188,7 @@ def load_mouse_bindings() -> KeyBindings: key_bindings = KeyBindings() @key_bindings.add(Keys.Vt100MouseEvent) - def _(event: E) -> None: + def _(event: E) -> "NotImplementedOrNone": """ Handling of incoming mouse event. """ @@ -235,7 +238,7 @@ def _(event: E) -> None: mouse_modifiers, ) = xterm_sgr_mouse_events[mouse_event, m] except KeyError: - return + return NotImplemented else: # Some other terminals, like urxvt, Hyper terminal, ... @@ -259,11 +262,16 @@ def _(event: E) -> None: try: y -= event.app.renderer.rows_above_layout except HeightIsUnknownError: - return + return NotImplemented # Call the mouse handler from the renderer. + + # Note: This can return `NotImplemented` if no mouse handler was + # found for this position, or if no repainting needs to + # happen. this way, we avoid excessive repaints during mouse + # movements. handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - handler( + return handler( MouseEvent( position=Point(x=x, y=y), event_type=mouse_event_type, @@ -272,6 +280,8 @@ def _(event: E) -> None: ) ) + return NotImplemented + @key_bindings.add(Keys.ScrollUp) def _scroll_up(event: E) -> None: """ @@ -289,7 +299,7 @@ def _scroll_down(event: E) -> None: event.key_processor.feed(KeyPress(Keys.Down), first=True) @key_bindings.add(Keys.WindowsMouseEvent) - def _mouse(event: E) -> None: + def _mouse(event: E) -> "NotImplementedOrNone": """ Handling of mouse events for Windows. """ @@ -317,8 +327,10 @@ def _mouse(event: E) -> None: y -= rows_above_cursor # Call the mouse event handler. + # (Can return `NotImplemented`.) handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - handler( + + return handler( MouseEvent( position=Point(x=x, y=y), event_type=event_type, @@ -327,4 +339,8 @@ def _mouse(event: E) -> None: ) ) + # No mouse handler found. Return `NotImplemented` so that we don't + # invalidate the UI. + return NotImplemented + return key_bindings diff --git a/prompt_toolkit/key_binding/key_bindings.py b/prompt_toolkit/key_binding/key_bindings.py index 7964c37f5d..06ca376b09 100644 --- a/prompt_toolkit/key_binding/key_bindings.py +++ b/prompt_toolkit/key_binding/key_bindings.py @@ -35,6 +35,7 @@ def my_key_binding(event): kb.add(Keys.A, my_key_binding) """ from abc import ABCMeta, abstractmethod, abstractproperty +from inspect import isawaitable from typing import ( TYPE_CHECKING, Awaitable, @@ -53,12 +54,27 @@ def my_key_binding(event): from prompt_toolkit.filters import FilterOrBool, Never, to_filter from prompt_toolkit.keys import KEY_ALIASES, Keys -# Avoid circular imports. if TYPE_CHECKING: + # Avoid circular imports. from .key_processor import KeyPressEvent + # The only two return values for a mouse hander (and key bindings) are + # `None` and `NotImplemented`. For the type checker it's best to annotate + # this as `object`. (The consumer never expects a more specific instance: + # checking for NotImplemented can be done using `is NotImplemented`.) + NotImplementedOrNone = object + # Other non-working options are: + # * Optional[Literal[NotImplemented]] + # --> Doesn't work, Literal can't take an Any. + # * None + # --> Doesn't work. We can't assign the result of a function that + # returns `None` to a variable. + # * Any + # --> Works, but too broad. + __all__ = [ + "NotImplementedOrNone", "Binding", "KeyBindingsBase", "KeyBindings", @@ -68,7 +84,13 @@ def my_key_binding(event): "GlobalOnlyKeyBindings", ] -KeyHandlerCallable = Callable[["KeyPressEvent"], Union[None, Awaitable[None]]] +# Key bindings can be regular functions or coroutines. +# In both cases, if they return `NotImplemented`, the UI won't be invalidated. +# This is mainly used in case of mouse move events, to prevent excessive +# repainting during mouse move events. +KeyHandlerCallable = Callable[ + ["KeyPressEvent"], Union["NotImplementedOrNone", Awaitable["NotImplementedOrNone"]] +] class Binding: @@ -102,8 +124,18 @@ def call(self, event: "KeyPressEvent") -> None: result = self.handler(event) # If the handler is a coroutine, create an asyncio task. - if result is not None: - event.app.create_background_task(result) + if isawaitable(result): + awaitable = cast(Awaitable["NotImplementedOrNone"], result) + + async def bg_task() -> None: + result = await awaitable + if result != NotImplemented: + event.app.invalidate() + + event.app.create_background_task(bg_task()) + + elif result != NotImplemented: + event.app.invalidate() def __repr__(self) -> str: return "%s(keys=%r, handler=%r)" % ( diff --git a/prompt_toolkit/key_binding/key_processor.py b/prompt_toolkit/key_binding/key_processor.py index 36f28fde6f..5d97c9ac7b 100644 --- a/prompt_toolkit/key_binding/key_processor.py +++ b/prompt_toolkit/key_binding/key_processor.py @@ -255,12 +255,9 @@ def get_next() -> KeyPress: else: return self.input_queue.popleft() - keys_processed = False is_flush = False while not_empty(): - keys_processed = True - # Process next key. key_press = get_next() @@ -277,16 +274,11 @@ def get_next() -> KeyPress: # an exception was raised) restart the processor for next time. self.reset() self.empty_queue() - app.invalidate() raise if not is_flush and not is_cpr: self.after_key_press.fire() - if keys_processed: - # Invalidate user interface. - app.invalidate() - # Skip timeout if the last key was flush. if not is_flush: self._start_timeout() diff --git a/prompt_toolkit/layout/containers.py b/prompt_toolkit/layout/containers.py index 19cc8012ac..bc1c00322c 100644 --- a/prompt_toolkit/layout/containers.py +++ b/prompt_toolkit/layout/containers.py @@ -61,7 +61,8 @@ if TYPE_CHECKING: from typing_extensions import Protocol, TypeGuard - NotImplementedOrNone = object + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + __all__ = [ "AnyContainer", @@ -1829,13 +1830,16 @@ def _write_to_screen_at_index( self.render_info = render_info # Set mouse handlers. - def mouse_handler(mouse_event: MouseEvent) -> None: - """Wrapper around the mouse_handler of the `UIControl` that turns - screen coordinates into line coordinates.""" + def mouse_handler(mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Wrapper around the mouse_handler of the `UIControl` that turns + screen coordinates into line coordinates. + Returns `NotImplemented` if no UI invalidation should be done. + """ # Don't handle mouse events outside of the current modal part of # the UI. if self not in get_app().layout.walk_through_modal_area(): - return + return NotImplemented # Find row/col position first. yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()} @@ -1882,7 +1886,9 @@ def mouse_handler(mouse_event: MouseEvent) -> None: # If it returns NotImplemented, handle it here. if result == NotImplemented: - self._mouse_handler(mouse_event) + result = self._mouse_handler(mouse_event) + + return result mouse_handlers.set_mouse_handler_for_range( x_min=write_position.xpos + sum(left_margin_widths), @@ -2552,15 +2558,22 @@ def do_scroll( ), ) - def _mouse_handler(self, mouse_event: MouseEvent) -> None: + def _mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": """ Mouse handler. Called when the UI control doesn't handle this particular event. + + Return `NotImplemented` if nothing was done as a consequence of this + key binding (no UI invalidate required in that case). """ if mouse_event.event_type == MouseEventType.SCROLL_DOWN: self._scroll_down() + return None elif mouse_event.event_type == MouseEventType.SCROLL_UP: self._scroll_up() + return None + + return NotImplemented def _scroll_down(self) -> None: "Scroll window down." diff --git a/prompt_toolkit/layout/controls.py b/prompt_toolkit/layout/controls.py index a0c16e1faa..1e1f15f7aa 100644 --- a/prompt_toolkit/layout/controls.py +++ b/prompt_toolkit/layout/controls.py @@ -32,12 +32,7 @@ split_lines, ) from prompt_toolkit.lexers import Lexer, SimpleLexer -from prompt_toolkit.mouse_events import ( - MouseButton, - MouseEvent, - MouseEventType, - MouseModifier, -) +from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType from prompt_toolkit.search import SearchState from prompt_toolkit.selection import SelectionType from prompt_toolkit.utils import get_cwidth @@ -53,23 +48,12 @@ ) if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase + from prompt_toolkit.key_binding.key_bindings import ( + KeyBindingsBase, + NotImplementedOrNone, + ) from prompt_toolkit.utils import Event - # The only two return values for a mouse hander are `None` and - # `NotImplemented`. For the type checker it's best to annotate this as - # `object`. (The consumer never expects a more specific instance: checking - # for NotImplemented can be done using `is NotImplemented`.) - NotImplementedOrNone = object - # Other non-working options are: - # * Optional[Literal[NotImplemented]] - # --> Doesn't work, Literal can't take an Any. - # * None - # --> Doesn't work. We can't assign the result of a function that - # returns `None` to a variable. - # * Any - # --> Works, but too broad. - __all__ = [ "BufferControl", diff --git a/prompt_toolkit/layout/menus.py b/prompt_toolkit/layout/menus.py index 0407f999f5..e138fcea11 100644 --- a/prompt_toolkit/layout/menus.py +++ b/prompt_toolkit/layout/menus.py @@ -40,9 +40,11 @@ from .margins import ScrollbarMargin if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import KeyBindings + from prompt_toolkit.key_binding.key_bindings import ( + KeyBindings, + NotImplementedOrNone, + ) - NotImplementedOrNone = object __all__ = [ "CompletionsMenu", diff --git a/prompt_toolkit/layout/mouse_handlers.py b/prompt_toolkit/layout/mouse_handlers.py index 9178341525..256231793a 100644 --- a/prompt_toolkit/layout/mouse_handlers.py +++ b/prompt_toolkit/layout/mouse_handlers.py @@ -1,15 +1,18 @@ from collections import defaultdict -from itertools import product -from typing import Callable, DefaultDict, Tuple +from typing import TYPE_CHECKING, Callable, DefaultDict from prompt_toolkit.mouse_events import MouseEvent +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + __all__ = [ "MouseHandler", "MouseHandlers", ] -MouseHandler = Callable[[MouseEvent], None] + +MouseHandler = Callable[[MouseEvent], "NotImplementedOrNone"] class MouseHandlers: @@ -18,10 +21,11 @@ class MouseHandlers: """ def __init__(self) -> None: - def dummy_callback(mouse_event: MouseEvent) -> None: + def dummy_callback(mouse_event: MouseEvent) -> "NotImplementedOrNone": """ :param mouse_event: `MouseEvent` instance. """ + return NotImplemented # NOTE: Previously, the data structure was a dictionary mapping (x,y) # to the handlers. This however would be more inefficient when copying @@ -38,7 +42,7 @@ def set_mouse_handler_for_range( x_max: int, y_min: int, y_max: int, - handler: Callable[[MouseEvent], None], + handler: Callable[[MouseEvent], "NotImplementedOrNone"], ) -> None: """ Set mouse handler for a region. diff --git a/prompt_toolkit/widgets/menus.py b/prompt_toolkit/widgets/menus.py index 9d53e9d322..7203aae118 100644 --- a/prompt_toolkit/widgets/menus.py +++ b/prompt_toolkit/widgets/menus.py @@ -16,7 +16,7 @@ Window, ) from prompt_toolkit.layout.controls import FormattedTextControl -from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType from prompt_toolkit.utils import get_cwidth from prompt_toolkit.widgets import Shadow