Repository: napari/napari

Issue:
Error when using the `Add polygons` mode for Shapes layer (`TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'`)
### 🐛 Bug Report

An error is triggered when using the `Add polygons` mode over a Shapes layer when adding a vertex in the same spot as the last one (while waiting a little bit before trying to add the new one)

### 💡 Steps to Reproduce

1. Create a Shapes layer
2. Select the `Add polygons` mode
3. Add a new vertex over the canvas and wait a couple of seconds without moving the mouse from its position
4. Try to add a new vertex over the same position


* Behavior preview:

![vertex](https://github.com/napari/napari/assets/16781833/f02a0cfb-ab68-422a-b92e-d7726a8cf1b1)



### 💡 Expected Behavior

To not see an error message and maybe the previous edge just be removed as when doing the mouse click quickly

### 🌎 Environment

```
napari: 0.5.0a2.dev517+g406aa9c8
Platform: Windows-10-10.0.19045-SP0
Python: 3.10.9 | packaged by conda-forge | (main, Jan 11 2023, 15:15:40) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.9
NumPy: 1.25.2
SciPy: 1.9.3
Dask: 2023.10.1
VisPy: 0.14.1
magicgui: 0.7.3
superqt: unknown
in-n-out: 0.1.5
app-model: 0.2.2
npe2: 0.7.3

OpenGL:
  - GL version:  4.5.0 - Build 25.20.100.6444
  - MAX_TEXTURE_SIZE: 16384

Screens:
  - screen 1: resolution 1366x768, scale 1.0
  - screen 2: resolution 1536x864, scale 1.0

Settings path:
  - C:\Users\dalth\AppData\Local\napari\napari-dev_4454fdbf170edefe23827a4bf512389a6806af69\settings.yaml
Plugins:
  - napari: 0.5.0a2.dev517+g406aa9c8 (81 contributions)
  - napari-assistant: 0.4.4 (4 contributions)
  - napari-console: 0.0.9 (0 contributions)
  - napari-generic-SIMulator: 0.0.19 (2 contributions)
  - napari-ome-zarr: 0.5.2 (2 contributions)
  - napari-pyclesperanto-assistant: 0.22.1 (51 contributions)
  - napari-skimage-regionprops: 0.10.0 (2 contributions)
  - napari-svg: 0.1.10 (2 contributions)
  - napari-time-slicer: 0.4.9 (2 contributions)
  - napari-tools-menu: 0.1.19 (0 contributions)
  - napari-ui-tracer: 0.2.0.dev0 (2 contributions)
```

### 💡 Additional Context

Traceback:

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\app\backends\_qt.py:496, in QtBaseCanvasBackend.mousePressEvent(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, ev=<PyQt5.QtGui.QMouseEvent object>)
    494 if self._vispy_canvas is None:
    495     return
--> 496 self._vispy_mouse_press(
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x000001E3D21793F0>
        ev = <PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670>
        BUTTONMAP = {0: 0, 1: 1, 2: 2, 4: 3, 8: 4, 16: 5}
    497     native=ev,
    498     pos=_get_event_xy(ev),
    499     button=BUTTONMAP.get(ev.button(), 0),
    500     modifiers=self._modifiers(ev),
    501 )

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\app\base.py:184, in BaseCanvasBackend._vispy_mouse_press(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, **kwargs={'button': 1, 'buttons': [], 'last_event': <MouseEvent blocked=False button=1 buttons=[1] d...ces=[] time=1705511204.736266 type=mouse_release>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (206, 147), 'press_event': None})
    181 def _vispy_mouse_press(self, **kwargs):
    182     # default method for delivering mouse press events to the canvas
    183     kwargs.update(self._vispy_mouse_data)
--> 184     ev = self._vispy_canvas.events.mouse_press(**kwargs)
        self._vispy_canvas.events.mouse_press = <vispy.util.event.EventEmitter object at 0x000001E3D2177DF0>
        kwargs = {'native': <PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670>, 'pos': (206, 147), 'button': 1, 'modifiers': (), 'buttons': [], 'press_event': None, 'last_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 0x000001E3DF404670> pos=[206 147] press_event=MouseEvent source=None sources=[] time=1705511204.736266 type=mouse_release>, 'last_mouse_press': None}
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x000001E3D21793F0>
        self._vispy_canvas.events = <vispy.util.event.EmitterGroup object at 0x000001E3D2177D30>
        self._vispy_canvas = <NapariSceneCanvas (PyQt5) at 0x1e3d2175bd0>
    185     if self._vispy_mouse_data['press_event'] is None:
    186         self._vispy_mouse_data['press_event'] = ev

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py:453, in EventEmitter.__call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'button': 1, 'buttons': [], 'last_event': <MouseEvent blocked=False button=1 buttons=[1] d...ces=[] time=1705511204.736266 type=mouse_release>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (206, 147), 'press_event': None})
    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=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670> pos=[206 147] press_event=None source=None sources=[] time=1705511205.706028 type=mouse_press>
        self = <vispy.util.event.EventEmitter object at 0x000001E3D2177DF0>
        cb = <bound method VispyCanvas._on_mouse_press of <napari._vispy.canvas.VispyCanvas object at 0x000001E3D2175BA0>>
    454 if event.blocked:
    455     break

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py:471, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCanvas._on_mouse_press of <napari._vispy.canvas.VispyCanvas object>>, event=<MouseEvent blocked=False button=1 buttons=[1] d...urces=[] time=1705511205.706028 type=mouse_press>)
    469     cb(event)
    470 except Exception:
--> 471     _handle_exception(self.ignore_callback_errors,
        self = <vispy.util.event.EventEmitter object at 0x000001E3D2177DF0>
        cb = <bound method VispyCanvas._on_mouse_press of <napari._vispy.canvas.VispyCanvas object at 0x000001E3D2175BA0>>
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670> pos=[206 147] press_event=None source=None sources=[] time=1705511205.706028 type=mouse_press>
        (cb, event) = (<bound method VispyCanvas._on_mouse_press of <napari._vispy.canvas.VispyCanvas object at 0x000001E3D2175BA0>>, <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670> pos=[206 147] press_event=None source=None sources=[] time=1705511205.706028 type=mouse_press>)
    472                       self.print_callback_errors,
    473                       self, cb_event=(cb, event))

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py:469, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCanvas._on_mouse_press of <napari._vispy.canvas.VispyCanvas object>>, event=<MouseEvent blocked=False button=1 buttons=[1] d...urces=[] time=1705511205.706028 type=mouse_press>)
    467 def _invoke_callback(self, cb, event):
    468     try:
--> 469         cb(event)
        cb = <bound method VispyCanvas._on_mouse_press of <napari._vispy.canvas.VispyCanvas object at 0x000001E3D2175BA0>>
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670> pos=[206 147] press_event=None source=None sources=[] time=1705511205.706028 type=mouse_press>
    470     except Exception:
    471         _handle_exception(self.ignore_callback_errors,
    472                           self.print_callback_errors,
    473                           self, cb_event=(cb, event))

File E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\canvas.py:461, in VispyCanvas._on_mouse_press(self=<napari._vispy.canvas.VispyCanvas object>, event=<MouseEvent blocked=False button=1 buttons=[1] d...urces=[] time=1705511205.706028 type=mouse_press>)
    449 def _on_mouse_press(self, event: MouseEvent) -> None:
    450     """Called whenever mouse pressed in canvas.
    451
    452     Parameters
   (...)
    459     None
    460     """
--> 461     self._process_mouse_event(mouse_press_callbacks, event)
        event = <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001E3DF404670> pos=[206 147] press_event=None source=None sources=[] time=1705511205.706028 type=mouse_press>
        self = <napari._vispy.canvas.VispyCanvas object at 0x000001E3D2175BA0>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\canvas.py:408, in VispyCanvas._process_mouse_event(self=<napari._vispy.canvas.VispyCanvas object>, mouse_callbacks=<function mouse_press_callbacks>, event=<ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent>)
    406 layer = self.viewer.layers.selection.active
    407 if layer is not None:
--> 408     mouse_callbacks(layer, event)
        event = <ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent at 0x000001E3D3FB9270>
        layer = <Shapes layer 'Shapes' at 0x1e3dd74f4f0>
        mouse_callbacks = <function mouse_press_callbacks at 0x000001E3CC438670>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\interactions.py:126, in mouse_press_callbacks(obj=<Shapes layer 'Shapes'>, event=<ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent>)
    123 # iterate through drag callback functions
    124 for mouse_drag_func in obj.mouse_drag_callbacks:
    125     # execute function to run press event code
--> 126     gen = mouse_drag_func(obj, event)
        mouse_drag_func = <function add_path_polygon at 0x000001E3CE60D090>
        obj = <Shapes layer 'Shapes' at 0x1e3dd74f4f0>
        event = <ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent at 0x000001E3D3FB9270>
    127     # if function returns a generator then try to iterate it
    128     if inspect.isgenerator(gen):

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shapes_mouse_bindings.py:445, in add_path_polygon(layer=<Shapes layer 'Shapes'>, event=<ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent>)
    443 index = layer._moving_value[0]
    444 new_type = Polygon if layer._mode == Mode.ADD_POLYGON else None
--> 445 add_vertex_to_path(layer, event, index, coordinates, new_type)
        coordinates = <class 'numpy.ndarray'> (2,) float64
        index = 0
        new_type = <class 'napari.layers.shapes._shapes_models.polygon.Polygon'>
        layer = <Shapes layer 'Shapes' at 0x1e3dd74f4f0>
        event = <ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent at 0x000001E3D3FB9270>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shapes_mouse_bindings.py:383, in add_vertex_to_path(layer=<Shapes layer 'Shapes'>, event=<ReadOnlyWrapper at 0x000001E3DF98B140 for MouseEvent>, index=0, coordinates=<class 'numpy.ndarray'> (2,) float64, new_type=<class 'napari.layers.shapes._shapes_models.polygon.Polygon'>)
    381 vertices = np.concatenate((vertices, [coordinates]), axis=0)
    382 value = layer.get_value(event.position, world=True)
--> 383 layer._value = (value[0], value[1] + 1)
        value = (None, None)
        layer._value = (None, None)
        layer = <Shapes layer 'Shapes' at 0x1e3dd74f4f0>
        value[0] = None
        value[1] = None
    384 layer._moving_value = copy(layer._value)
    385 layer._data_view.edit(index, vertices, new_type=new_type)

TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
```


Discussion:

Resolving patch:
diff --git a/napari/layers/shapes/_shapes_mouse_bindings.py b/napari/layers/shapes/_shapes_mouse_bindings.py
--- a/napari/layers/shapes/_shapes_mouse_bindings.py
+++ b/napari/layers/shapes/_shapes_mouse_bindings.py
@@ -437,13 +437,20 @@ def add_path_polygon(layer: Shapes, event: MouseEvent) -> None:
     # on press
     coordinates = layer.world_to_data(event.position)
     if layer._is_creating is False:
+        # Set last cursor position to initial position of the mouse when starting to draw the shape
+        layer._last_cursor_position = np.array(event.pos)
         # Start drawing a path
         initiate_polygon_draw(layer, coordinates)
     else:
         # Add to an existing path or polygon
         index = layer._moving_value[0]
         new_type = Polygon if layer._mode == Mode.ADD_POLYGON else None
-        add_vertex_to_path(layer, event, index, coordinates, new_type)
+        # Ensure the position of the new vertex is different from the previous
+        # one before adding it. See napari/napari#6597
+        if not np.array_equal(
+            np.array(event.pos), layer._last_cursor_position
+        ):
+            add_vertex_to_path(layer, event, index, coordinates, new_type)
 
 
 def move_active_vertex_under_cursor(
diff --git a/napari/layers/shapes/shapes.py b/napari/layers/shapes/shapes.py
--- a/napari/layers/shapes/shapes.py
+++ b/napari/layers/shapes/shapes.py
@@ -2590,17 +2590,24 @@ def _finish_drawing(self, event=None) -> None:
         self._fixed_vertex = None
         self._value = (None, None)
         self._moving_value = (None, None)
+        self._last_cursor_position = None
         if self._is_creating is True:
             if self._mode == Mode.ADD_PATH:
                 vertices = self._data_view.shapes[index].data
                 if len(vertices) <= 2:
                     self._data_view.remove(index)
+                    # Clear selected data to prevent issues.
+                    # See https://github.com/napari/napari/pull/6912#discussion_r1601169680
+                    self.selected_data.clear()
                 else:
                     self._data_view.edit(index, vertices[:-1])
             if self._mode in {Mode.ADD_POLYGON, Mode.ADD_POLYGON_LASSO}:
                 vertices = self._data_view.shapes[index].data
                 if len(vertices) <= 3:
                     self._data_view.remove(index)
+                    # Clear selected data to prevent issues.
+                    # See https://github.com/napari/napari/pull/6912#discussion_r1601169680
+                    self.selected_data.clear()
                 elif self._mode == Mode.ADD_POLYGON:
                     self._data_view.edit(index, vertices[:-1])
                 else:


Test patch:
diff --git a/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py b/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py
--- a/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py
+++ b/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py
@@ -234,16 +234,19 @@ def test_add_complex_shape(shape_type, create_known_shapes_layer):
         event = read_only_mouse_event(
             type='mouse_move',
             position=coord,
+            pos=np.array(coord, dtype=float),
         )
         mouse_move_callbacks(layer, event)
         event = read_only_mouse_event(
             type='mouse_press',
             position=coord,
+            pos=np.array(coord, dtype=float),
         )
         mouse_press_callbacks(layer, event)
         event = read_only_mouse_event(
             type='mouse_release',
             position=coord,
+            pos=np.array(coord, dtype=float),
         )
         mouse_release_callbacks(layer, event)
 
@@ -266,6 +269,62 @@ def test_add_complex_shape(shape_type, create_known_shapes_layer):
     assert layer.selected_data == {n_shapes}
 
 
+@pytest.mark.parametrize(
+    'shape_type_vertices',
+    [
+        ('path', [[20, 30], [20, 30]]),
+        ('polygon', [[20, 30], [10, 50], [10, 50]]),
+    ],
+)
+def test_add_invalid_shape(shape_type_vertices, create_known_shapes_layer):
+    """Check invalid shape clicking behavior in add polygon mode."""
+    layer, n_shapes, known_non_shape = create_known_shapes_layer
+
+    # Add shape at location where non exists
+    shape_type, shape_vertices = shape_type_vertices
+    layer.mode = f'add_{shape_type}'
+
+    for coord in shape_vertices:
+        # Simulate move, click, and release
+        event = read_only_mouse_event(
+            type='mouse_move',
+            position=coord,
+            pos=np.array(coord, dtype=float),
+        )
+        mouse_move_callbacks(layer, event)
+        event = read_only_mouse_event(
+            type='mouse_press',
+            position=coord,
+            pos=np.array(coord, dtype=float),
+        )
+        mouse_press_callbacks(layer, event)
+        event = read_only_mouse_event(
+            type='mouse_release',
+            position=coord,
+            pos=np.array(coord, dtype=float),
+        )
+        mouse_release_callbacks(layer, event)
+
+    # Although the shape/polygon being created is invalid, three shapes should
+    # be available until it is marked as finished via mouse double click
+    assert len(layer.data) == n_shapes + 1
+
+    # finish drawing causing the removal of the in progress shape since is invalid
+    end_click = read_only_mouse_event(
+        type='mouse_double_click',
+        position=coord,
+        pos=np.array(coord, dtype=float),
+    )
+    assert layer.mouse_double_click_callbacks
+    mouse_double_click_callbacks(layer, end_click)
+
+    # Ensure no data is selected, number of shapes is the initial one and
+    # last cursor position variable was reset
+    assert len(layer.selected_data) == 0
+    assert len(layer.data) == n_shapes
+    assert layer._last_cursor_position is None
+
+
 def test_vertex_insert(create_known_shapes_layer):
     """Add vertex to shape."""
     layer, n_shapes, known_non_shape = create_known_shapes_layer