-
Notifications
You must be signed in to change notification settings - Fork 732
Better Mouse Support #1387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Better Mouse Support #1387
Conversation
…riminate 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.
I literally /just/ hacked together a crappier version of this PR. I would really love to see this merged |
Thanks a lot! This sounds great. I hope to find some time soon to review and merge this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good already! But some improvements would be good.
@@ -10,6 +15,30 @@ | |||
"load_mouse_bindings", | |||
] | |||
|
|||
# fmt: off |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you remove fmt: off
markings. I prefer to not use them, sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, discard this question. It's fine.
|
||
# TODO: Is it possible to add modifiers here? | ||
# fmt: off | ||
mouse_button,mouse_event_type,mouse_modifier = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this lookup table become a global constant? (The function is getting rather long otherwise).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
}.get((mouse_event, m)) | ||
# fmt: off | ||
# flake8: noqa E201 | ||
mouse_button,mouse_event_type,mouse_modifier = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this become a global constant as well?
Maybe fmt: off
is justified here to align the comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
prompt_toolkit/mouse_events.py
Outdated
SHIFT_CONTROL = (MouseModifierKey.SHIFT, MouseModifierKey.CONTROL,) | ||
ALT_CONTROL = ( MouseModifierKey.ALT,MouseModifierKey.CONTROL,) | ||
SHIFT_ALT_CONTROL = (MouseModifierKey.SHIFT,MouseModifierKey.ALT,MouseModifierKey.CONTROL,) | ||
UNKNOWN_MODIFIER = ("UNKNOWN") # This is used if we're not sure what modifiers are being used, if any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a tuple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One could alternatively use IntFlag
for Modifiers:
class MouseModifierKey(IntFlag):
SHIFT = 1
ALT = 2
CONTROL = 4
class MouseModifier(Enum):
NO_MODIFIER = 0
SHIFT = MouseModifierKey.SHIFT
ALT = MouseModifierKey.ALT
SHIFT_ALT = MouseModifierKey.SHIFT | MouseModifierKey.ALT
CONTROL = MouseModifierKey.CONTROL
SHIFT_CONTROL = MouseModifierKey.SHIFT | MouseModifierKey.CONTROL
ALT_CONTROL = MouseModifierKey.ALT | MouseModifierKey.CONTROL
SHIFT_ALT_CONTROL = MouseModifierKey.SHIFT | MouseModifierKey.ALT | MouseModifierKey.CONTROL
UNKNOWN_MODIFIER = "UNKNOWN"
If that's cleaner or not, I don't know.
position: Point, | ||
event_type: MouseEventType, | ||
button: MouseButton, | ||
modifier: MouseModifier, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it makes sense for modifier
to be an enum. Set[MouseModifier]
seems more logical.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set[MouseModifier] makes more semantic sense, but the reason I decided to do it this way is because Set[MouseModifier] isn't hashable - meaning you can't use Set[MouseModifier] as keys in a dictionary to branch off efficiently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have frozenset for that. ;)
While I merge it, I'll probably do some polishing, and use frozen sets. If that's fine for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just looked into that...yeah I think that would work well! I don't know how I missed that feature lol:) I don't think changing them from tuples to frozensets will affect the functionality of the code, so I think it should be safe to change it if you want
prompt_toolkit/mouse_events.py
Outdated
LEFT = "LEFT" | ||
MIDDLE = "MIDDLE" | ||
RIGHT = "RIGHT" | ||
NO_BUTTON = "" # When we're scrolling, or just moving the mouse and not pressing a button, mouse_event.button=="". The reason it's an empty string is so that bool(MouseButton.NO_BUTTON)==False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put the comments above the constants, and wrap them at 80 characters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that people would rely on bool(MouseButton.NO_BUTTON)==False
. I don't think mypy would approve it either. Can you make it NO_BUTTON = "NO_BUTTON"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
modifier=mouse_modifier, | ||
) | ||
) | ||
# print(MouseEvent(position=Point(x=x, y=y), event_type=mouse_event_type, button=mouse_button, modifier=mouse_modifier)) # Uncomment to debug mouse events |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remove this line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No button mouse drag events have a lowercase m
for me. (MOUSE_MOVE instead of MOUSE_DRAG might be a better name?)
Hi Jonathan, I made a revision to this pull request in the 5th commit - please take a look! |
I also added a small new feature, which gives dropdown menus more interactivity! It acts more like windows or mac now; when you click the 'file' menu for example, the menu items will be highlighted as you drag your mouse down (even if you don't click). Likewise, you can now press your mouse down on a 'file' menu, and release it on the 'save' menu item - and it will trigger (no need to click twice, like in Ubuntu, Windows or Mac). This small addition is a good use case for this better mouse support. I've made a video to demonstrate this functionality: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this gets merged, one can add support to windows with the extra modifiers by updating ConsoleInputReader._handle_mouse
in win32.py
(partial implementation as an example):
def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> List[KeyPress]:
FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001
RIGHTMOST_BUTTON_PRESSED = 0x0002
FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004
RIGHT_ALT_PRESSED = 0x0001
LEFT_ALT_PRESSED = 0x0002
RIGHT_CTRL_PRESSED = 0x0004
LEFT_CTRL_PRESSED = 0x0008
SHIFT_PRESSED = 0x0010
MOUSE_MOVED = 0x0001
MOUSE_WHEELED = 0x0004
# Event type
if ev.EventFlags & MOUSE_MOVED:
event_type = MouseEventType.MOUSE_MOVE
elif ev.EventFlags & MOUSE_WHEELED:
if ev.ButtonState > 0:
event_type = MouseEventType.SCROLL_UP
else:
event_type = MouseEventType.SCROLL_DOWN
elif not ev.ButtonState:
event_type = MouseEventType.MOUSE_UP
else:
event_type = MouseEventType.MOUSE_DOWN
# Buttons
if not ev.ButtonState:
button = MouseButton.NO_BUTTON
elif ev.ButtonState & FROM_LEFT_1ST_BUTTON_PRESSED:
button = MouseButton.LEFT
elif ev.ButtonState & RIGHTMOST_BUTTON_PRESSED:
button = MouseButton.RIGHT
# More buttons here:
# https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str?redirectedfrom=MSDN
# For now, just assume middle-mouse button.
else:
button = MouseButton.MIDDLE
# Modifiers (If using IntFlag Enum for modifier keys)
mods = 0
if ev.ControlKeyState & LEFT_ALT_PRESSED or ev.ControlKeyState & RIGHT_ALT_PRESSED:
mods |= MouseModifierKey.ALT
if ev.ControlKeyState & LEFT_CTRL_PRESSED or ev.ControlKeyState & RIGHT_CTRL_PRESSED:
mods |= MouseModifierKey.CONTROL
if ev.ControlKeyState & SHIFT_PRESSED:
mods |= MouseModifierKey.SHIFT
modifier = MouseModifier(mods)
...
# Package data somehow
modifier=mouse_modifier, | ||
) | ||
) | ||
# print(MouseEvent(position=Point(x=x, y=y), event_type=mouse_event_type, button=mouse_button, modifier=mouse_modifier)) # Uncomment to debug mouse events |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No button mouse drag events have a lowercase m
for me. (MOUSE_MOVE instead of MOUSE_DRAG might be a better name?)
@salt-die In the newest commit, I actually changed MOUSE_DRAG to MOUSE_MOVE lol - I agree, MOUSE_MOVE is probably a better name. As for using numbers to represent the mouse modifiers, you make a good point - it would be nice to combine them using | . However, that would also make the mouse modifiers unreadable if you tried to print them without internal knowledge of how prompt_toolkit works. For example, if somebody prints ALT_CONTROL for debugging and gets 6, it doesn't tell you very much. I think we should leave that decision to @jonathanslenders |
Printing isn't that bad with IntFlag enums, one gets a slightly better repr than just an int: In [61]: MouseModifier.SHIFT_ALT
Out[61]: <MouseModifier.SHIFT_ALT: <MouseModifierKey.ALT|SHIFT: 3>>
In [62]: MouseModifier.SHIFT_ALT_CONTROL
Out[62]: <MouseModifier.SHIFT_ALT_CONTROL: <MouseModifierKey.CONTROL|ALT|SHIFT: 7>> |
Hi @RyannDaGreat , this looks great! Is there any chance you can rebase on the master branch? There are a lot of formatting changes, which makes the code hard to review. I'm not sure what's the easiest way to rebase. |
Hi @jonathanslenders! I've never rebased anything before, so this is a bit new to me...would this be done as an additional commit? (I just started looking up documentation at https://git-scm.com/book/en/v2/Git-Branching-Rebasing) |
In that case, maybe leave it like it is. I'll see if I get some time, and can do it myself in the coming week. |
Ok, thank you! Let me know if there's anything else I can do to help. |
Yes, I am. But a few important things to keep into account before I can merge these things:
Hope that makes sense. |
I rebased these changes on master. See this PR: I'll try to take it from here. |
In this pull request, I 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.
To help you understand the difference more clearly, I've made a youtube video showing what my pull request does:
https://www.youtube.com/watch?v=Qe76evU5VgA