Repository: napari/napari

Issue:
feature request: point selection event
## 🚀 Feature
I would like an event to be fired when the selection changes on the points layer

## Motivation

I would like to use the point selection as the model and single source of truth for selection in a plugin. There is no event on selection change.

## Pitch

Add an event for selection change

## Alternatives

there is a 'highlight' event but this is fired when hovering over a point too





Discussion:
we *could* use a psygnal `EventedSet` internally for the selection model - if we don't want to mix event systems we could keep it private and hook it up to a vispy event emitter
I'm pro EventedSet and I'm not super concerned about mixing event systems... If we wait to migrate *all* events in one go we are going to be waiting a long time 😂 

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
@@ -1,3 +1,5 @@
+from typing import Set, TypeVar
+
 import numpy as np
 
 from napari.layers.points._points_utils import _points_in_box_3d, points_in_box
@@ -127,27 +129,32 @@ def highlight(layer, event):
     layer._set_highlight()
 
 
-def _toggle_selected(selected_data, value):
-    """Add or remove value from the selected data set.
+_T = TypeVar("_T")
+
+
+def _toggle_selected(selection: Set[_T], value: _T) -> Set[_T]:
+    """Add or remove value from the selection set.
+
+    This function returns a copy of the existing selection.
 
     Parameters
     ----------
-    selected_data : set
+    selection: set
         Set of selected data points to be modified.
     value : int
         Index of point to add or remove from selected data set.
 
     Returns
     -------
-    set
-        Modified selected_data set.
+    selection: set
+        Updated selection.
     """
-    if value in selected_data:
-        selected_data.remove(value)
+    selection = set(selection)
+    if value in selection:
+        selection.remove(value)
     else:
-        selected_data.add(value)
-
-    return selected_data
+        selection.add(value)
+    return selection
 
 
 def _update_drag_vectors_from_event(layer, event):
diff --git a/napari/layers/points/points.py b/napari/layers/points/points.py
--- a/napari/layers/points/points.py
+++ b/napari/layers/points/points.py
@@ -6,6 +6,7 @@
 
 import numpy as np
 import pandas as pd
+from psygnal.containers import Selection
 from scipy.stats import gmean
 
 from napari.layers.base import Layer, no_op
@@ -450,7 +451,7 @@ def __init__(
         self._shown = np.empty(0).astype(bool)
 
         # Indices of selected points
-        self._selected_data = set()
+        self._selected_data: Selection[int] = Selection()
         self._selected_data_stored = set()
         self._selected_data_history = set()
         # Indices of selected points within the currently viewed slice
@@ -1273,13 +1274,14 @@ def _get_state(self):
         return state
 
     @property
-    def selected_data(self) -> set:
+    def selected_data(self) -> Selection[int]:
         """set: set of currently selected points."""
         return self._selected_data
 
     @selected_data.setter
-    def selected_data(self, selected_data):
-        self._selected_data = set(selected_data)
+    def selected_data(self, selected_data: Sequence[int]) -> None:
+        self._selected_data.clear()
+        self._selected_data.update(set(selected_data))
         self._selected_view = list(
             np.intersect1d(
                 np.array(list(self._selected_data)),


Test patch:
diff --git a/napari/layers/points/_tests/test_points.py b/napari/layers/points/_tests/test_points.py
--- a/napari/layers/points/_tests/test_points.py
+++ b/napari/layers/points/_tests/test_points.py
@@ -5,6 +5,7 @@
 import numpy as np
 import pandas as pd
 import pytest
+from psygnal.containers import Selection
 from pydantic import ValidationError
 from vispy.color import get_colormap
 
@@ -2528,3 +2529,12 @@ def test_editable_and_visible_are_independent():
     layer.visible = True
 
     assert not layer.editable
+
+
+def test_point_selection_remains_evented_after_update():
+    """Existing evented selection model should be updated rather than replaced."""
+    data = np.empty((3, 2))
+    layer = Points(data)
+    assert isinstance(layer.selected_data, Selection)
+    layer.selected_data = {0, 1}
+    assert isinstance(layer.selected_data, Selection)