mwcraig / ipyevents

A custom widget for returning mouse and keyboard events to Python. Documentation:
https://ipyevents.readthedocs.io/en/latest/index.html
BSD 3-Clause "New" or "Revised" License
112 stars 27 forks source link

Mousemove events not emitted after shift+mousedown with button 1 in Jupyter Lab #74

Open edwardwkrohne opened 1 year ago

edwardwkrohne commented 1 year ago

I'm trying to allow the user to "draw" a line across a widget, and have the shift key have special meaning. I check mousemove events with button 1 depressed to do regular drawing, but for shift+draw, no events are emitted by ipyevents.

The following code has been modified from the example notebook.

from ipywidgets import Label, HTML, HBox, Image, VBox, Box, HBox
from ipyevents import Event 
from IPython.display import display

l = Label('Move the mouse across me!')
l.layout.border = '2px solid red'

h = HTML('Event info')
d = Event(source=l, watched_events=['mousemove'])

def handle_event(event):
    lines = ['{}: {}'.format(k, v) for k, v in event.items()]
    content = '<br>'.join(lines)
    h.value = content

d.on_dom_event(handle_event)

display(l, h)

In Jupyter Lab,

In Jupyter Notebook, all events I just listed are displayed appropriately. Adding prevent_default_action=True had no effect.

!jupyter --version

produces

Selected Jupyter core packages...
IPython          : 8.3.0
ipykernel        : 6.13.0
ipywidgets       : 7.7.0
jupyter_client   : 7.3.1
jupyter_core     : 4.10.0
jupyter_server   : 1.17.0
jupyterlab       : 3.5.0b0
nbclient         : 0.6.3
nbconvert        : 6.5.0
nbformat         : 5.4.0
notebook         : 6.4.11
qtconsole        : 5.3.0
traitlets        : 5.2.1.post0

and

import ipyevents
ipyevents.__version__

produces

'2.0.1'

I upgraded from JupyterLab 3.4.0 to make this report; I got identical behavior in both versions.

mwcraig commented 1 year ago

Thanks for the report @edwardwkrohne -- I'm hoping to fix it this week while I'm at a workshop with some jupyterlab devs.

mwcraig commented 1 year ago

With some help from Afshin Darian I found the root cause of this. It turns out the notebook in JupyterLab captures mousemove events when

This happens on the mousedown event, which is why the order of pressing the shift key matters: if you are not holding the shift key when you press the left mouse button then the event capture does not happen. If you have selected some text before pushing the shift key it also doesn't happen.

There is not a great workaround, though Darian said jupyterlab might be willing to add an API for turning off the event capture in this case.

I'm not sure why jupyterlab grabs this case beyond this comment.

I'll try to think of a workaround in ipyevents, though it would be a little hacky and I'm not sure I can actually get it to work, or what it might break...

edwardwkrohne commented 1 year ago

Thank you! This is great to know.

Since you've read the code surrounding this already, what do you think my odds would be if I were to make a source install of JupyterLab and comment out that one line? Would it be sufficient to do shift+draw? Would it have unpleasant consequences? Looking at the blame and the PR the line was written at, it was added to address things like browser context menus showing up when the Jupyter one needed to be shown.

mwcraig commented 1 year ago

With the caveat that I'm not an expert in this code base, I think the line that prevents the event from getting to ipyevents is here: https://github.com/jupyterlab/jupyterlab/blob/4b8f2de55ee74754920c675140153d51ded985ab/packages/notebook/src/widget.ts#L2217

That said, commenting out the stopPropagation might well break drag-and-drop of notebook tabs and stuff.

One thought would be to move the stopPopagation inside the switch statement.

Another thought, which would mean not having to modify jupyterlab itself, would be to select some text in the output cell whenever the mouse enters the element that ipyevents is attached to. If something is selected when this conditional is hit, then that branch is not entered, and the mousemove listener that stops propagation isn't added.

I don't of a great way to make that selection happen, though I think it will have to be on the js side, not the python side.