Repository: napari/napari

Issue:
Copy and paste multiple points doesn't work
I'm trying to copy and paste multiple points on Napari, but it doesn't work.
I have searched in _Points layer_ shortcuts menu, but copy and past commands missed.

How to fix?


Version: 
**napari**: 0.4.17rc8
**Python**: 3.8.16 | packaged by conda-forge | (default, Feb 1 2023, 15:53:35) [MSC v.1929 64 bit (AMD64)]

Thank you in advance


Discussion:
Thanks for the bug report! Do you have any more details to share? Any of the following would be helpful:

- Code that causes this issue.
- A video showing the problem occurring.
- Any error or log messages from napari.

Also, what do you mean by the *Points layer* shortcuts menu? 

After some quick experimentation, I found one issue when copying and pasting, which can be reproduced by running

```python
from napari.layers import Points

points = Points([[0, 0]])
points.selected_data = {0}
for i in range(5):
    points._copy_data()
    points._paste_data()
```

which produces the error `IndexError: index 4 is out of bounds for axis 0 with size 4`. The important detail here is the number `5`, since this bug is related to the dimensionality of color values.

But would be good to confirm it's the same bug. If it is, I think the fix is easy enough. Happy to open a PR for that or label this as a good-ish first issue.
I'm using deeplabcut with napari.

The "Point layer menu" is in file>preferences>shortcut

I don't see any errors, but the software doesn't copy/paste points... there is the copy/paste functionality for "shape layer", but I have to use points to do my project (anyway I don't code any lines of code).
@nricciardi 
Despite what the shortcuts would lead you to believe, you can copy paste points—at least on macOS. You just need to select them using the select tool and control-C (or command on macOS) will copy, control-V paste.
But, there is no visual indicator. You can drag them to see that they were copied & pasted.

But, it's only true as long as there is less then 5 ☹️ 

Edit2: it's pathological that we could have different keybinds for copy-paste per layer, so this needs to be standardized on the viewer or left to the OS to handle.


I can reproduce this without deeplabcut.
It's sufficient to make a points layer, make two points, select them, and try to control-C to copy.
If you make 4 or fewer points, you can select them and copy paste.
If you make 5 points or more, then when you try to copy paste even a single point you get an IndexError:
Edit: Whoops, I think i'm saying the same thing as @andy-sweet 


```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
File ~/Dev/vispy/vispy/app/backends/_qt.py:561, in QtBaseCanvasBackend.keyPressEvent(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, ev=<PyQt5.QtGui.QKeyEvent object>)
    560 def keyPressEvent(self, ev):
--> 561     self._keyEvent(self._vispy_canvas.events.key_press, ev)
        self._vispy_canvas.events.key_press = <vispy.util.event.EventEmitter object at 0x281374d00>
        ev = <PyQt5.QtGui.QKeyEvent object at 0x29475a4d0>
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x28135f640>
        self._vispy_canvas.events = <vispy.util.event.EmitterGroup object at 0x281374b50>
        self._vispy_canvas = <NapariSceneCanvas (PyQt5) at 0x281366ad0>

File ~/Dev/vispy/vispy/app/backends/_qt.py:622, in QtBaseCanvasBackend._keyEvent(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, func=<vispy.util.event.EventEmitter object>, ev=<PyQt5.QtGui.QKeyEvent object>)
    620     key = None
    621 mod = self._modifiers(ev)
--> 622 func(native=ev, key=key, text=str(ev.text()), modifiers=mod)
        key = <Key 'C'>
        mod = (<Key 'Control'>,)
        ev = <PyQt5.QtGui.QKeyEvent object at 0x29475a4d0>
        func = <vispy.util.event.EventEmitter object at 0x281374d00>

File ~/Dev/vispy/vispy/util/event.py:453, in EventEmitter.__call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'key': <Key 'C'>, 'modifiers': (<Key 'Control'>,), 'native': <PyQt5.QtGui.QKeyEvent object>, 'text': ''})
    450 if self._emitting > 1:
    451     raise RuntimeError('EventEmitter loop detected!')
--> 453 self._invoke_callback(cb, event)
        event = <KeyEvent blocked=False handled=False key=<Key 'C'> modifiers=(<Key 'Control'>,) native=<PyQt5.QtGui.QKeyEvent object at 0x29475a4d0> source=None sources=[] text= type=key_press>
        self = <vispy.util.event.EventEmitter object at 0x281374d00>
        cb = <bound method KeymapHandler.on_key_press of <napari.utils.key_bindings.KeymapHandler object at 0x281335cc0>>
    454 if event.blocked:
    455     break

File ~/Dev/vispy/vispy/util/event.py:471, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method KeymapHandler.on_key_press of <napari.utils.key_bindings.KeymapHandler object>>, event=<KeyEvent blocked=False handled=False key=<Key '...4d0> source=None sources=[] text= type=key_press>)
    469     cb(event)
    470 except Exception:
--> 471     _handle_exception(self.ignore_callback_errors,
        self = <vispy.util.event.EventEmitter object at 0x281374d00>
        cb = <bound method KeymapHandler.on_key_press of <napari.utils.key_bindings.KeymapHandler object at 0x281335cc0>>
        event = <KeyEvent blocked=False handled=False key=<Key 'C'> modifiers=(<Key 'Control'>,) native=<PyQt5.QtGui.QKeyEvent object at 0x29475a4d0> source=None sources=[] text= type=key_press>
        (cb, event) = (<bound method KeymapHandler.on_key_press of <napari.utils.key_bindings.KeymapHandler object at 0x281335cc0>>, <KeyEvent blocked=False handled=False key=<Key 'C'> modifiers=(<Key 'Control'>,) native=<PyQt5.QtGui.QKeyEvent object at 0x29475a4d0> source=None sources=[] text= type=key_press>)
    472                       self.print_callback_errors,
    473                       self, cb_event=(cb, event))

File ~/Dev/vispy/vispy/util/event.py:469, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method KeymapHandler.on_key_press of <napari.utils.key_bindings.KeymapHandler object>>, event=<KeyEvent blocked=False handled=False key=<Key '...4d0> source=None sources=[] text= type=key_press>)
    467 def _invoke_callback(self, cb, event):
    468     try:
--> 469         cb(event)
        cb = <bound method KeymapHandler.on_key_press of <napari.utils.key_bindings.KeymapHandler object at 0x281335cc0>>
        event = <KeyEvent blocked=False handled=False key=<Key 'C'> modifiers=(<Key 'Control'>,) native=<PyQt5.QtGui.QKeyEvent object at 0x29475a4d0> source=None sources=[] text= type=key_press>
    470     except Exception:
    471         _handle_exception(self.ignore_callback_errors,
    472                           self.print_callback_errors,
    473                           self, cb_event=(cb, event))

File ~/Dev/napari/napari/utils/key_bindings.py:504, in KeymapHandler.on_key_press(self=<napari.utils.key_bindings.KeymapHandler object>, event=<KeyEvent blocked=False handled=False key=<Key '...4d0> source=None sources=[] text= type=key_press>)
    494 if (
    495     event.native is not None
    496     and event.native.isAutoRepeat()
   (...)
    500     # unless the combo being held down is one of the autorepeatables or
    501     # one of the navigation keys (helps with scrolling).
    502     return
--> 504 self.press_key(kb)
        kb = <app_model.types._keys._keybindings.KeyBinding object at 0x294c84d00>
        self = <napari.utils.key_bindings.KeymapHandler object at 0x281335cc0>

File ~/Dev/napari/napari/utils/key_bindings.py:426, in KeymapHandler.press_key(self=<napari.utils.key_bindings.KeymapHandler object>, key_bind=<app_model.types._keys._keybindings.KeyBinding object>)
    417 if not callable(func):
    418     raise TypeError(
    419         trans._(
    420             "expected {func} to be callable",
   (...)
    423         )
    424     )
--> 426 generator_or_callback = func()
        func = <bound method copy of <Points layer 'Points' at 0x17e86ba00>>
    428 key = str(key_bind.parts[-1].key)
    430 if inspect.isgeneratorfunction(func):

File ~/Dev/napari/napari/layers/points/_points_key_bindings.py:54, in copy(layer=<Points layer 'Points'>)
     51 @Points.bind_key(KeyMod.CtrlCmd | KeyCode.KeyC, overwrite=True)
     52 def copy(layer: Points):
     53     """Copy any selected points."""
---> 54     layer._copy_data()
        layer = <Points layer 'Points' at 0x17e86ba00>

File ~/Dev/napari/napari/layers/points/points.py:2055, in Points._copy_data(self=<Points layer 'Points'>)
   2043 if len(self.selected_data) > 0:
   2044     index = list(self.selected_data)
   2045     self._clipboard = {
   2046         'data': deepcopy(self.data[index]),
   2047         'edge_color': deepcopy(self.edge_color[index]),
   2048         'face_color': deepcopy(self.face_color[index]),
   2049         'shown': deepcopy(self.shown[index]),
   2050         'size': deepcopy(self.size[index]),
   2051         'symbol': deepcopy(self.symbol[index]),
   2052         'edge_width': deepcopy(self.edge_width[index]),
   2053         'features': deepcopy(self.features.iloc[index]),
   2054         'indices': self._slice_indices,
-> 2055         'text': self.text._copy(index),
        self._clipboard = {'data': <class 'numpy.ndarray'> (1, 2) float64, 'edge_color': <class 'numpy.ndarray'> (1, 4) float64, 'face_color': <class 'numpy.ndarray'> (1, 4) float64, 'shown': <class 'numpy.ndarray'> (1,) bool, 'size': <class 'numpy.ndarray'> (1, 2) int64, 'symbol': <class 'numpy.ndarray'> (1,) object, 'edge_width': <class 'numpy.ndarray'> (1,) float64, 'features': Empty DataFrame
Columns: []
Index: [3], 'indices': (slice(None, None, None), slice(None, None, None)), 'text': {'string': <class 'numpy.ndarray'> () <U1, 'color': <class 'numpy.ndarray'> (1,) float32}}
        self = <Points layer 'Points' at 0x17e86ba00>
        index = [4]
   2056     }
   2057 else:
   2058     self._clipboard = {}

File ~/Dev/napari/napari/layers/utils/text_manager.py:218, in TextManager._copy(self=TextManager(string=ConstantStringEncoding(consta...<class 'numpy.ndarray'> () float64, rotation=0.0), indices=[4])
    214 def _copy(self, indices: List[int]) -> dict:
    215     """Copies all encoded values at the given indices."""
    216     return {
    217         'string': _get_style_values(self.string, indices),
--> 218         'color': _get_style_values(self.color, indices),
        self.string = ConstantStringEncoding(constant=<class 'numpy.ndarray'> () <U1, encoding_type='ConstantStringEncoding')
        indices = [4]
        self = TextManager(string=ConstantStringEncoding(constant=<class 'numpy.ndarray'> () <U1, encoding_type='ConstantStringEncoding'), color=ConstantColorEncoding(constant=<class 'numpy.ndarray'> (4,) float32, encoding_type='ConstantColorEncoding'), visible=True, size=12, blending=<Blending.TRANSLUCENT: 'translucent'>, anchor=<Anchor.CENTER: 'center'>, translation=<class 'numpy.ndarray'> () float64, rotation=0.0)
        self.color = ConstantColorEncoding(constant=<class 'numpy.ndarray'> (4,) float32, encoding_type='ConstantColorEncoding')
    219     }

File ~/Dev/napari/napari/layers/utils/style_encoding.py:289, in _get_style_values(encoding=ConstantColorEncoding(constant=<class 'numpy.nda...) float32, encoding_type='ConstantColorEncoding'), indices=[4], value_ndim=0)
    287 """Returns a scalar style value or indexes non-scalar style values."""
    288 values = encoding._values
--> 289 return values if values.ndim == value_ndim else values[indices]
        values = <class 'numpy.ndarray'> (4,) float32
        value_ndim = 0
        indices = [4]

IndexError: index 4 is out of bounds for axis 0 with size 4
```
I will try with 5 points... thanks you

Resolving patch:
diff --git a/napari/layers/utils/text_manager.py b/napari/layers/utils/text_manager.py
--- a/napari/layers/utils/text_manager.py
+++ b/napari/layers/utils/text_manager.py
@@ -215,7 +215,7 @@ def _copy(self, indices: List[int]) -> dict:
         """Copies all encoded values at the given indices."""
         return {
             'string': _get_style_values(self.string, indices),
-            'color': _get_style_values(self.color, indices),
+            'color': _get_style_values(self.color, indices, value_ndim=1),
         }
 
     def _paste(self, *, string: StringArray, color: ColorArray):


Test patch:
diff --git a/napari/layers/utils/_tests/test_text_manager.py b/napari/layers/utils/_tests/test_text_manager.py
--- a/napari/layers/utils/_tests/test_text_manager.py
+++ b/napari/layers/utils/_tests/test_text_manager.py
@@ -676,10 +676,14 @@ def test_refresh_with_derived_color():
 
 def test_copy_paste_with_constant_color():
     color = {'constant': 'blue'}
-    features = pd.DataFrame(index=range(3))
+    features = pd.DataFrame(index=range(5))
     text_manager = TextManager(color=color, features=features)
 
-    copied = text_manager._copy([0, 2])
+    # Use an index of 4 to ensure that constant color values, which are
+    # RGBA 4-vectors, are handled correctly when copied, unlike in a
+    # related bug:
+    # https://github.com/napari/napari/issues/5786
+    copied = text_manager._copy([0, 4])
     text_manager._paste(**copied)
 
     actual = text_manager.color._values