Repository: napari/napari

Issue:
Points layer allows panning when in Add mode
## 🐛 Bug
When in the `Add Points` mode on a points layer panning is not prevented. This can make it tricky to label a point as you have to hold the mouse perfectly still.

![Peek 2021-03-08 13-26](https://user-images.githubusercontent.com/10111092/110364681-111fde80-8012-11eb-8a22-65e5451c87fd.gif)

In contrast the `pick` mode of a labels layer prevents panning.

## To Reproduce
1. Open napari
2. Add a points layer
3. Move mouse around and then click

Alternatively:
`python examples/nD_points.py`

## Expected behavior
Clicking when in `Add Points` mode will not allow panning, the same as when when in `Pick` mode for a labels layer.


## Environment

```
napari: 0.4.7.dev14+gc473058
Platform: Linux-5.8.0-7630-generic-x86_64-with-glibc2.31
System: Pop!_OS 20.04 LTS
Python: 3.9.1 | packaged by conda-forge | (default, Jan 26 2021, 01:34:10) [GCC 9.3.0]
Qt: 5.15.2
PyQt5: 5.15.2
NumPy: 1.19.5
SciPy: 1.6.0
Dask: 2021.01.1
VisPy: 0.6.6

OpenGL:
- GL version: 4.6 (Compatibility Profile) Mesa 20.2.6
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1080, scale 1.0

Plugins:
- console: 0.0.3
- svg: 0.1.4
```

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Agreed @ianhi. This was recently added in #2148 after a request in #2146, but the threshold for dragging is too sensitive. We might be able to overwrite the threshold in vispy and increase it? I'm not sure if @VolkerH or @jni have further ideas
This is what I mentioned in the PR  #2148 as

> One potentially unwanted side effect is that "fly-by" adding of points is no longer possible, i.e. the mouse pointer must
> come to a halt before clicking

As I noticed the potential issue at the time I remember thinking briefly for workarounds, but I would have to dig deeper into the code again and I don't think I have time. The previous behaviour was worse IMO though, when working with images that were substantially larger then the example shown above. I guess it depends what one wants to achieve: accurately marking points on a large canvas or flyby clicking a few points on a smaller canvas (I'm thinking space invaders). Ideally the behaviour would support both.
We could adjust the https://doc.qt.io/qt-5/qstylehints.html#startDragVelocity-prop (or https://doc.qt.io/qt-5/qstylehints.html#startDragDistance-prop) to try and find a better value. I wonder if this can be done just for the canvas, or it can only be done at the application level (cc @tlambert03 @goanpeca)

Resolving patch:
diff --git a/napari/layers/points/_points_mouse_bindings.py b/napari/layers/points/_points_mouse_bindings.py
--- a/napari/layers/points/_points_mouse_bindings.py
+++ b/napari/layers/points/_points_mouse_bindings.py
@@ -81,19 +81,20 @@ def select(layer, event):
     layer._set_highlight(force=True)
 
 
+DRAG_DIST_THRESHOLD = 5
+
+
 def add(layer, event):
     """Add a new point at the clicked position."""
-    # on press
-    dragged = False
-    yield
 
-    # on move
-    while event.type == 'mouse_move':
-        dragged = True
+    if event.type == 'mouse_press':
+        start_pos = event.pos
+
+    while event.type != 'mouse_release':
         yield
 
-    # on release
-    if not dragged:
+    dist = np.linalg.norm(start_pos - event.pos)
+    if dist < DRAG_DIST_THRESHOLD:
         coordinates = layer.world_to_data(event.position)
         layer.add(coordinates)
 


Test patch:
diff --git a/napari/layers/points/_tests/test_points_mouse_bindings.py b/napari/layers/points/_tests/test_points_mouse_bindings.py
--- a/napari/layers/points/_tests/test_points_mouse_bindings.py
+++ b/napari/layers/points/_tests/test_points_mouse_bindings.py
@@ -1,4 +1,5 @@
-import collections
+from dataclasses import dataclass, field
+from typing import List, Tuple
 
 import numpy as np
 import pytest
@@ -12,19 +13,15 @@
 )
 
 
-@pytest.fixture
-def Event():
-    """Create a subclass for simulating vispy mouse events.
+@dataclass
+class Event:
+    """Create a subclass for simulating vispy mouse events."""
 
-    Returns
-    -------
-    Event : Type
-        A new tuple subclass named Event that can be used to create a
-        NamedTuple object with fields "type", "is_dragging", and "modifiers".
-    """
-    return collections.namedtuple(
-        'Event', field_names=['type', 'is_dragging', 'modifiers', 'position']
-    )
+    type: str
+    is_dragging: bool = False
+    modifiers: List[str] = field(default_factory=list)
+    position: Tuple[int, int] = (0, 0)  # world coords
+    pos: np.ndarray = np.zeros(2)  # canvas coords
 
 
 @pytest.fixture
@@ -54,31 +51,17 @@ def create_known_points_layer():
     return layer, n_points, known_non_point
 
 
-def test_not_adding_or_selecting_point(create_known_points_layer, Event):
+def test_not_adding_or_selecting_point(create_known_points_layer):
     """Don't add or select a point by clicking on one in pan_zoom mode."""
     layer, n_points, _ = create_known_points_layer
     layer.mode = 'pan_zoom'
 
     # Simulate click
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=(0, 0),
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_press'))
     mouse_press_callbacks(layer, event)
 
     # Simulate release
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=(0, 0),
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_release'))
     mouse_release_callbacks(layer, event)
 
     # Check no new point added and non selected
@@ -86,7 +69,7 @@ def test_not_adding_or_selecting_point(create_known_points_layer, Event):
     assert len(layer.selected_data) == 0
 
 
-def test_add_point(create_known_points_layer, Event):
+def test_add_point(create_known_points_layer):
     """Add point by clicking in add mode."""
     layer, n_points, known_non_point = create_known_points_layer
 
@@ -95,23 +78,13 @@ def test_add_point(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_press', position=known_non_point)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate release
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_release', position=known_non_point)
     )
     mouse_release_callbacks(layer, event)
 
@@ -120,7 +93,7 @@ def test_add_point(create_known_points_layer, Event):
     np.testing.assert_allclose(layer.data[-1], known_non_point)
 
 
-def test_drag_in_add_mode(create_known_points_layer, Event):
+def test_drag_in_add_mode(create_known_points_layer):
     """Drag in add mode and make sure no point is added."""
     layer, n_points, known_non_point = create_known_points_layer
 
@@ -130,12 +103,7 @@ def test_drag_in_add_mode(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_press', position=known_non_point)
     )
     mouse_press_callbacks(layer, event)
 
@@ -144,10 +112,7 @@ def test_drag_in_add_mode(create_known_points_layer, Event):
     # Simulate drag end
     event = ReadOnlyWrapper(
         Event(
-            type='mouse_move',
-            is_dragging=True,
-            modifiers=[],
-            position=known_non_point_end,
+            type='mouse_move', is_dragging=True, position=known_non_point_end
         )
     )
     mouse_move_callbacks(layer, event)
@@ -156,9 +121,8 @@ def test_drag_in_add_mode(create_known_points_layer, Event):
     event = ReadOnlyWrapper(
         Event(
             type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
             position=known_non_point_end,
+            pos=np.array([4, 4]),
         )
     )
     mouse_release_callbacks(layer, event)
@@ -167,7 +131,7 @@ def test_drag_in_add_mode(create_known_points_layer, Event):
     assert len(layer.data) == n_points
 
 
-def test_select_point(create_known_points_layer, Event):
+def test_select_point(create_known_points_layer):
     """Select a point by clicking on one in select mode."""
     layer, n_points, _ = create_known_points_layer
 
@@ -175,25 +139,11 @@ def test_select_point(create_known_points_layer, Event):
     position = tuple(layer.data[0])
 
     # Simulate click
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
     mouse_press_callbacks(layer, event)
 
     # Simulate release
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
     mouse_release_callbacks(layer, event)
 
     # Check clicked point selected
@@ -201,7 +151,7 @@ def test_select_point(create_known_points_layer, Event):
     assert 0 in layer.selected_data
 
 
-def test_after_in_add_mode_point(create_known_points_layer, Event):
+def test_after_in_add_mode_point(create_known_points_layer):
     """Don't add or select a point by clicking on one in pan_zoom mode."""
     layer, n_points, _ = create_known_points_layer
 
@@ -210,25 +160,11 @@ def test_after_in_add_mode_point(create_known_points_layer, Event):
     position = tuple(layer.data[0])
 
     # Simulate click
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
     mouse_press_callbacks(layer, event)
 
     # Simulate release
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
     mouse_release_callbacks(layer, event)
 
     # Check no new point added and non selected
@@ -236,7 +172,7 @@ def test_after_in_add_mode_point(create_known_points_layer, Event):
     assert len(layer.selected_data) == 0
 
 
-def test_after_in_select_mode_point(create_known_points_layer, Event):
+def test_after_in_select_mode_point(create_known_points_layer):
     """Don't add or select a point by clicking on one in pan_zoom mode."""
     layer, n_points, _ = create_known_points_layer
 
@@ -245,25 +181,11 @@ def test_after_in_select_mode_point(create_known_points_layer, Event):
     position = tuple(layer.data[0])
 
     # Simulate click
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
     mouse_press_callbacks(layer, event)
 
     # Simulate release
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
     mouse_release_callbacks(layer, event)
 
     # Check no new point added and non selected
@@ -271,7 +193,7 @@ def test_after_in_select_mode_point(create_known_points_layer, Event):
     assert len(layer.selected_data) == 0
 
 
-def test_unselect_select_point(create_known_points_layer, Event):
+def test_unselect_select_point(create_known_points_layer):
     """Select a point by clicking on one in select mode."""
     layer, n_points, _ = create_known_points_layer
 
@@ -280,25 +202,11 @@ def test_unselect_select_point(create_known_points_layer, Event):
     layer.selected_data = {2, 3}
 
     # Simulate click
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
     mouse_press_callbacks(layer, event)
 
     # Simulate release
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=position,
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
     mouse_release_callbacks(layer, event)
 
     # Check clicked point selected
@@ -306,7 +214,7 @@ def test_unselect_select_point(create_known_points_layer, Event):
     assert 0 in layer.selected_data
 
 
-def test_add_select_point(create_known_points_layer, Event):
+def test_add_select_point(create_known_points_layer):
     """Add to a selection of points point by shift-clicking on one."""
     layer, n_points, _ = create_known_points_layer
 
@@ -316,23 +224,13 @@ def test_add_select_point(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=['Shift'],
-            position=position,
-        )
+        Event(type='mouse_press', modifiers=['Shift'], position=position)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate release
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=['Shift'],
-            position=position,
-        )
+        Event(type='mouse_release', modifiers=['Shift'], position=position)
     )
     mouse_release_callbacks(layer, event)
 
@@ -341,7 +239,7 @@ def test_add_select_point(create_known_points_layer, Event):
     assert layer.selected_data == {2, 3, 0}
 
 
-def test_remove_select_point(create_known_points_layer, Event):
+def test_remove_select_point(create_known_points_layer):
     """Remove from a selection of points point by shift-clicking on one."""
     layer, n_points, _ = create_known_points_layer
 
@@ -351,23 +249,13 @@ def test_remove_select_point(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=['Shift'],
-            position=position,
-        )
+        Event(type='mouse_press', modifiers=['Shift'], position=position)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate release
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=['Shift'],
-            position=position,
-        )
+        Event(type='mouse_release', modifiers=['Shift'], position=position)
     )
     mouse_release_callbacks(layer, event)
 
@@ -376,7 +264,7 @@ def test_remove_select_point(create_known_points_layer, Event):
     assert layer.selected_data == {2, 3}
 
 
-def test_not_selecting_point(create_known_points_layer, Event):
+def test_not_selecting_point(create_known_points_layer):
     """Don't select a point by not clicking on one in select mode."""
     layer, n_points, known_non_point = create_known_points_layer
 
@@ -384,23 +272,13 @@ def test_not_selecting_point(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_press', position=known_non_point)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate release
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_release', position=known_non_point)
     )
     mouse_release_callbacks(layer, event)
 
@@ -408,7 +286,7 @@ def test_not_selecting_point(create_known_points_layer, Event):
     assert len(layer.selected_data) == 0
 
 
-def test_unselecting_points(create_known_points_layer, Event):
+def test_unselecting_points(create_known_points_layer):
     """Unselect points by not clicking on one in select mode."""
     layer, n_points, known_non_point = create_known_points_layer
 
@@ -418,23 +296,13 @@ def test_unselecting_points(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_press', position=known_non_point)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate release
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_release', position=known_non_point)
     )
     mouse_release_callbacks(layer, event)
 
@@ -442,7 +310,7 @@ def test_unselecting_points(create_known_points_layer, Event):
     assert len(layer.selected_data) == 0
 
 
-def test_selecting_all_points_with_drag(create_known_points_layer, Event):
+def test_selecting_all_points_with_drag(create_known_points_layer):
     """Select all points when drag box includes all of them."""
     layer, n_points, known_non_point = create_known_points_layer
 
@@ -450,50 +318,29 @@ def test_selecting_all_points_with_drag(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_press', position=known_non_point)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate drag start
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_move',
-            is_dragging=True,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_move', is_dragging=True, position=known_non_point)
     )
     mouse_move_callbacks(layer, event)
 
     # Simulate drag end
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_move', is_dragging=True, modifiers=[], position=(0, 0)
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_move', is_dragging=True))
     mouse_move_callbacks(layer, event)
 
     # Simulate release
-    event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=True,
-            modifiers=[],
-            position=(0, 0),
-        )
-    )
+    event = ReadOnlyWrapper(Event(type='mouse_release', is_dragging=True))
     mouse_release_callbacks(layer, event)
 
     # Check all points selected as drag box contains them
     assert len(layer.selected_data) == n_points
 
 
-def test_selecting_no_points_with_drag(create_known_points_layer, Event):
+def test_selecting_no_points_with_drag(create_known_points_layer):
     """Select all points when drag box includes all of them."""
     layer, n_points, known_non_point = create_known_points_layer
 
@@ -501,45 +348,25 @@ def test_selecting_no_points_with_drag(create_known_points_layer, Event):
 
     # Simulate click
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_press',
-            is_dragging=False,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_press', position=known_non_point)
     )
     mouse_press_callbacks(layer, event)
 
     # Simulate drag start
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_move',
-            is_dragging=True,
-            modifiers=[],
-            position=known_non_point,
-        )
+        Event(type='mouse_move', is_dragging=True, position=known_non_point)
     )
     mouse_move_callbacks(layer, event)
 
     # Simulate drag end
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_move',
-            is_dragging=True,
-            modifiers=[],
-            position=(50, 60),
-        )
+        Event(type='mouse_move', is_dragging=True, position=(50, 60))
     )
     mouse_move_callbacks(layer, event)
 
     # Simulate release
     event = ReadOnlyWrapper(
-        Event(
-            type='mouse_release',
-            is_dragging=True,
-            modifiers=[],
-            position=(50, 60),
-        )
+        Event(type='mouse_release', is_dragging=True, position=(50, 60))
     )
     mouse_release_callbacks(layer, event)