Repository: napari/napari

Issue:
[0.4.18rc1] Dragging Points layer in add-points mode emits TypeError
## 🐛 Bug

Dragging Points layer in add-points mode emits "TypeError: cannot set attribute handled" with v0.4.18.
It seems there's a bug in mouse drag callbacks.

## To Reproduce

Steps to reproduce the behavior:

1. Add a new points layer.
2. Click "Add points" button in the layer control.
3. Drag.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

Should work.

## Environment

 - Please copy and paste the information at napari info option in help menubar here:

```
napari: 0.4.18rc1
Platform: Windows-10-10.0.22621-SP0
Python: 3.11.0 | packaged by Anaconda, Inc. | (main, Mar 1 2023, 18:18:21) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.9
NumPy: 1.23.5
SciPy: 1.10.1
Dask: 2023.6.0
VisPy: 0.12.2
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.7
app-model: 0.1.4
npe2: 0.7.0

OpenGL:
- GL version: 4.6.0 NVIDIA 512.15
- MAX_TEXTURE_SIZE: 32768

Screens:
- screen 1: resolution 1920x1080, scale 1.0

Settings path:
- C:\Users\Uemura-Lab\AppData\Local\napari\mt_6d1d31bd47bd05cf06330b327e4414e52c348d6e\settings.yaml
```

 - Any other relevant information:

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
@hanjinliu thanks for the report! Would you mind:
- providing the full traceback?
- checking whether this error happens in main and/or 0.4.17?

At any rate my first guess is the error might come from #5742. But it's not clear whether the error propagated in main or whether we just forgot to cherry-pick related PRs. 
Here's a script to try it out quickly:

```python
import napari
import numpy as np

viewer = napari.Viewer()
pts_layer = viewer.add_points(np.random.random((5, 2)) * 512)

pts_layer.mode = 'add'
napari.run()
```

Things seem to be working fine for me on main...
Here's the trackback I got in my env.

<details><summary>Traceback</summary><div>

```
---------------------------------------------------
TypeError         Traceback (most recent call last)
File ~\mambaforge\envs\mt\Lib\site-packages\vispy\app\backends\_qt.py:532, in QtBaseCanvasBackend.mouseMoveEvent(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, ev=<PyQt5.QtGui.QMouseEvent object>)
    530 if self._vispy_canvas is None:
    531     return
--> 532 self._vispy_mouse_move(
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x00000248F9ABDD80>
        ev = <PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0>
    533     native=ev,
    534     pos=_get_event_xy(ev),
    535     modifiers=self._modifiers(ev),
    536 )

File ~\mambaforge\envs\mt\Lib\site-packages\vispy\app\base.py:216, in BaseCanvasBackend._vispy_mouse_move(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, **kwargs={'button': 1, 'buttons': [1], 'last_event': <MouseEvent blocked=False button=1 buttons=[1] d...rces=[] time=1687484171.1154914 type=mouse_press>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (535, 398), 'press_event': <MouseEvent blocked=False button=1 buttons=[1] d...rces=[] time=1687484171.1154914 type=mouse_press>})
    213 else:
    214     kwargs['button'] = self._vispy_mouse_data['press_event'].button
--> 216 ev = self._vispy_canvas.events.mouse_move(**kwargs)
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x00000248F9ABDD80>
        self._vispy_canvas.events.mouse_move = <vispy.util.event.EventEmitter object at 0x00000248FA3E92D0>
        kwargs = {'native': <PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0>, 'pos': (535, 398), 'modifiers': (), 'buttons': [1], 'press_event': <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=True is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[537 399] press_event=None source=None sources=[] time=1687484171.1154914 type=mouse_press>, 'last_event': <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=True is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[537 399] press_event=None source=None sources=[] time=1687484171.1154914 type=mouse_press>, 'last_mouse_press': None, 'button': 1}
        self._vispy_canvas.events = <vispy.util.event.EmitterGroup object at 0x00000248FA3E8C10>      
        self._vispy_canvas = <VispyCanvas (PyQt5) at 0x248fa3cea50>
    217 self._vispy_mouse_data['last_event'] = ev  
    218 return ev

File ~\mambaforge\envs\mt\Lib\site-packages\vispy\util\event.py:453, in EventEmitter.__call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'button': 1, 'buttons': [1], 'last_event': <MouseEvent blocked=False button=1 buttons=[1] d...rces=[] time=1687484171.1154914 type=mouse_press>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (535, 398), 'press_event': <MouseEvent blocked=False button=1 buttons=[1] d...rces=[] time=1687484171.1154914 type=mouse_press>})
    450 if self._emitting > 1:
    451     raise RuntimeError('EventEmitter loop detected!')
--> 453 self._invoke_callback(cb, event)
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=True last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[535 398] press_event=MouseEvent source=None sources=[] time=1687484171.173613 type=mouse_move>  
        self = <vispy.util.event.EventEmitter object at 0x00000248FA3E92D0>
        cb = <bound method QtViewer.on_mouse_move of <napari._qt.qt_viewer.QtViewer object at 0x00000248F9ABC310>>
    454 if event.blocked:
    455     break

File ~\mambaforge\envs\mt\Lib\site-packages\vispy\util\event.py:471, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method QtViewer.on_mouse_move of <napari._qt.qt_viewer.QtViewer object>>, event=<MouseEvent blocked=False button=1 buttons=[1] d...ources=[] time=1687484171.173613 type=mouse_move>)
    469     cb(event)
    470 except Exception:
--> 471     _handle_exception(self.ignore_callback_errors,                                            
        self = <vispy.util.event.EventEmitter object at 0x00000248FA3E92D0>
        cb = <bound method QtViewer.on_mouse_move of <napari._qt.qt_viewer.QtViewer object at 0x00000248F9ABC310>>
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=True last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[535 398] press_event=MouseEvent source=None sources=[] time=1687484171.173613 type=mouse_move>  
        (cb, event) = (<bound method QtViewer.on_mouse_move of <napari._qt.qt_viewer.QtViewer object at 0x00000248F9ABC310>>, <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=True last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[535 398] press_event=MouseEvent source=None sources=[] time=1687484171.173613 type=mouse_move>)
    472                       self.print_callback_errors,                                             
    473                       self, cb_event=(cb, event))                                             

File ~\mambaforge\envs\mt\Lib\site-packages\vispy\util\event.py:469, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method QtViewer.on_mouse_move of <napari._qt.qt_viewer.QtViewer object>>, event=<MouseEvent blocked=False button=1 buttons=[1] d...ources=[] time=1687484171.173613 type=mouse_move>)
    467 def _invoke_callback(self, cb, event):     
    468     try:
--> 469         cb(event)
        cb = <bound method QtViewer.on_mouse_move of <napari._qt.qt_viewer.QtViewer object at 0x00000248F9ABC310>>
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=True last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[535 398] press_event=MouseEvent source=None sources=[] time=1687484171.173613 type=mouse_move>  
    470     except Exception:
    471         _handle_exception(self.ignore_callback_errors,
    472                           self.print_callback_errors,
    473                           self, cb_event=(cb, event))

File ~\mambaforge\envs\mt\Lib\site-packages\napari\_qt\qt_viewer.py:1198, in QtViewer.on_mouse_move(self=<napari._qt.qt_viewer.QtViewer object>, event=<MouseEvent blocked=False button=1 buttons=[1] d...ources=[] time=1687484171.173613 type=mouse_move>)   
   1190 def on_mouse_move(self, event):
   1191     """Called whenever mouse moves over canvas.
   1192
   1193     Parameters
   (...)
   1196         The vispy event that triggered this method.
   1197     """
-> 1198     self._process_mouse_event(mouse_move_callbacks, event)                                    
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=True last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x00000248860763B0> pos=[535 398] press_event=MouseEvent source=None sources=[] time=1687484171.173613 type=mouse_move>  
        self = <napari._qt.qt_viewer.QtViewer object at 0x00000248F9ABC310>

File ~\mambaforge\envs\mt\Lib\site-packages\napari\_qt\qt_viewer.py:1147, in QtViewer._process_mouse_event(self=<napari._qt.qt_viewer.QtViewer object>, mouse_callbacks=<function mouse_move_callbacks>, event=<ReadOnlyWrapper at 0x000002488CB83D80 for MouseEvent>)
   1145 layer = self.viewer.layers.selection.active
   1146 if layer is not None:
-> 1147     mouse_callbacks(layer, event)
        event = <ReadOnlyWrapper at 0x000002488CB83D80 for MouseEvent at 0x000002488CB127D0>
        layer = <Points layer 'Points' at 0x2488c24d790>
        mouse_callbacks = <function mouse_move_callbacks at 0x00000248F72BCEA0>

File ~\mambaforge\envs\mt\Lib\site-packages\napari\utils\interactions.py:173, in mouse_move_callbacks(obj=<Points layer 'Points'>, event=<ReadOnlyWrapper at 0x000002488CB83D80 for MouseEvent>)
    170 obj._persisted_mouse_event[gen].__wrapped__
 = event
    171 try:
    172     # try to advance the generator
--> 173     next(gen)
        gen = <generator object add at 0x000002488C0164D0>
    174 except StopIteration:
    175     # If done deleted the generator and stored event
    176     del obj._mouse_drag_gen[func]

File ~\mambaforge\envs\mt\Lib\site-packages\napari\layers\points\_points_mouse_bindings.py:122, in add(layer=<Points layer 'Points'>, event=<ReadOnlyWrapper at 0x000002488CB12A80 for ReadOnlyWrapper>)    
    119     dist = np.linalg.norm(start_pos - event.pos)
    120     if dist < DRAG_DIST_THRESHOLD:
    121         # prevent vispy from moving the canvas if we're below threshold
--> 122         event.handled = True
        event = <ReadOnlyWrapper at 0x000002488CB12A80 for ReadOnlyWrapper at 0x000002488CB83D80>     
    123     yield
    125 # in some weird cases you might have press and release without move,
    126 # so we just make 100% sure dist is correct

File ~\mambaforge\envs\mt\Lib\site-packages\napari\utils\_proxies.py:29, in ReadOnlyWrapper.__setattr__(self=<ReadOnlyWrapper at 0x000002488CB12A80 for ReadOnlyWrapper>, name='handled', val=True)
     24 def __setattr__(self, name, val):
     25     if (
     26         name not in ('__wrapped__', '_self_exceptions')
     27         and name not in self._self_exceptions
     28     ):
---> 29         raise TypeError(
        trans = <napari.utils.translations.TranslationBundle object at 0x00000248F1234C50>
        name = 'handled'
     30             trans._(
     31                 'cannot set attribute {name}',
     32                 deferred=True,
     33                 name=name,
     34             )
     35         )
     37     super().__setattr__(name, val)

TypeError: cannot set attribute handled
```

---
</div></details>

hmmm. Why is your event in a ReadOnlyWrapper and mine isn't... 🤔 
Oh sorry, I missed your first comment.
I didn't see this bug in 0.4.17.
I just tested in the main branch and, the bug's somehow fixed 🤔
Ok, so I guess there must have been a bug fix after #5742 that was not cherry-picked to the 0.4.18 branch. CC @Czaki 
I can confirm that the error exists immediately after #5742 (fb7da0a1eb9e418b049f3fbf1d4043de59950291). So now it's just a matter of bisecting between fb7da0a1eb9e418b049f3fbf1d4043de59950291 and main!
Ok should have just checked the backlinks at the bottom of #5742 😂. The fix is in #5797 which was indeed labeled for 0.5.0 instead of 0.4.18. I have updated the milestone, @Czaki!

Resolving patch:
diff --git a/napari/_qt/qt_viewer.py b/napari/_qt/qt_viewer.py
--- a/napari/_qt/qt_viewer.py
+++ b/napari/_qt/qt_viewer.py
@@ -1139,7 +1139,7 @@ def _process_mouse_event(self, mouse_callbacks, event):
         event.dims_point = list(self.viewer.dims.point)
 
         # Put a read only wrapper on the event
-        event = ReadOnlyWrapper(event)
+        event = ReadOnlyWrapper(event, exceptions=('handled',))
         mouse_callbacks(self.viewer, event)
 
         layer = self.viewer.layers.selection.active


Test patch: