Skip to content

_my_layer

labelLayer

Bases: QObject

Source code in src/napari_layer_table/_my_layer.py
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
class labelLayer(QtCore.QObject):

    # Note: these have to be kept in sync with mmLayer signals
    signalDataChanged = QtCore.Signal(object, object, object, pd.DataFrame)
    # action'
    # selected set
    # selected data : not used
    # selected df

    signalLayerNameChange = QtCore.Signal(str)

    def __init__(self, viewer, layer,
                        onAddCallback=None,
                        onDeleteCallback=None,):
        """
        Args:
            onAddCallback: not implemented
            onDeleteCallback: not implemented
        """
        #super().__init__(viewer, layer)
        super().__init__()

        self._viewer = viewer
        self._layer = layer

        # for label layer, this is an integer (not a set)
        self._selected_label = self._layer.selected_label

        # just show one selected label (hide all others)
        #self._layer.show_selected_label = True

        self._connectLayer()

    def bringToFront(self):
        if self._viewer.layers.selection.active != self._layer:
            self._viewer.layers.selection.active = self._layer

    def getName(self):
        return self._layer.name

    def _copy_data(self):
        logger.info('labelLayer NOT IMPLEMENTED')

    def slot_selected_label(self, event):
        """Respond to user setting label in viewer.
        """
        logger.info('labelLayer')
        selected_label = self._layer.selected_label # int

        if selected_label == self._selected_label:
            print('  no new label selection')
            return

        self._selected_label = selected_label
        print('  _selected_label:', self._selected_label)

        #properties = self.getDataFrame()

        #print('  properties:')
        #pprint(properties)

        '''
        print('  event.source', event.source)  # same as self._layer
        print('  event.type', event.type)
        print('  selected_label:', self._layer.selected_label)
        print('  event.source.selected_label:', event.source.selected_label)
        '''

        self._selected_label = event.source._selected_label  # int

        properties = self.getDataFrame()
        print('  -->> emit "select"')
        # in label layer we will only every select one label
        # signal/slot expects a list
        selectedLabelList = [self._selected_label]
        emptyDict = dict()
        self.signalDataChanged.emit('select', selectedLabelList, emptyDict, properties)

        #_vars = vars(event)
        #pprint(_vars)

    def getDataFrame_all(self) -> pd.DataFrame:
        # TODO (cudmore) this does not work !!!
        logger.info('label layer')
        return self.getDataFrame(getFull=True)

    def getDataFrame(self, getFull=False) -> pd.DataFrame:
        # self._layer.features gives us a (features, properties) pandas dataframe !!!

        logger.info(f'label layer getFull:{getFull}')

        dfFeatures = self._layer.features  # all features

        if getFull:
            #selectedList = range(len(self._layer.data))
            # TODO (cudmore) consider keeeping track of this as a member _numLabels
            selectedList = np.unique(self._layer.data).tolist()  # list
        else:
            selectedList = [self._selected_label]  # int

        # label index is 1 based, add one to list
        # TODO (cudmore) our data model does not work with row labels 1,2,3,...
        #  it s expecting row index 0 to be preesent
        #  need to switch over all .iLoc[] to .loc[] or similar
        '''
        if selectedList:
            selectedList = selectedList[0:-1]
            selectedList = [index+1 for index in selectedList]
        '''
        # drop 0 from list
        zeroIndex = selectedList.index(0)
        if zeroIndex is not None:
            logger.info('removing 0 from list, label layer starts at 1')
            del selectedList[zeroIndex]

        #print('  selectedList:', selectedList)
        #print('  dfFeatures:')
        #pprint(dfFeatures)

        if len(dfFeatures) == 0:
            dfFeatures = pd.DataFrame(index=selectedList)

        # reduce by selection
        df = dfFeatures.loc[selectedList]

        df.loc[selectedList, "label"] = selectedList

        # TODO (cudmore) add z/y/x as mean of pixels in label

        # TODO (cudmore add color with layer get_color(idx) as 'Face Color'
        colorList_rgba = [self._layer.get_color(index)
                        for index in selectedList]

        if selectedList:
            colorList_rgba[0] = (1., 1., 1., 1.)  # index 0 is not actually a label (it selects all)
        # use str(rgb_to_hex(oneColor)[0])
        colorList_hex = [str(rgb_to_hex(oneColor)[0])
                        for oneColor in colorList_rgba]

        #print('  colorList_hex:', colorList_hex)
        df.loc[selectedList, "Face Color"] = colorList_hex

        # abb cudmore baltimore, adding region props to table
        # Note: region props does not return row 0
        _properties = ['label', 'centroid', 'area']  # 'solidity' gives convex-hull error
        props_dict = regionprops_table(self._layer.data, properties=_properties)
        dfProps = pd.DataFrame(props_dict)
        # rename some columns
        dfProps = dfProps.rename(columns={'centroid-0': 'z', 'centroid-1': 'y', 'centroid-2': 'z'})

        #print('df:')
        #pprint(df)

        return df

    def slot_user_edit_name(self, event):
        logger.info('')
        #newName = self._layer.name
        newName = event.source.name
        self.signalLayerNameChange.emit(newName)

    def _connectLayer(self, layer=None):
        self._layer.events.name.connect(self.slot_user_edit_name)
        self._layer.events.selected_label.connect(self.slot_selected_label)


    def selectItems(self, selectedRowSet : set):
        """Set the selected items in layer.

        Called from _my_widget on user selectnig row(s) in table
        """
        #self._layer.selected_data = selectedRowSet
        selectedRowList = list(selectedRowSet)
        if selectedRowList:
            selectedRow = selectedRowList[0]
            logger.info(f'labelLayer selectedRow: {selectedRow}')
            self._layer.selected_label = selectedRow

    def snapToItem(self, selectedRow : int, isAlt : bool =False):
        pass

    def doUndo(self):
        #self._undo.doUndo()
        pass

__init__(viewer, layer, onAddCallback=None, onDeleteCallback=None)

Parameters:

Name Type Description Default
onAddCallback

not implemented

None
onDeleteCallback

not implemented

None
Source code in src/napari_layer_table/_my_layer.py
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
def __init__(self, viewer, layer,
                    onAddCallback=None,
                    onDeleteCallback=None,):
    """
    Args:
        onAddCallback: not implemented
        onDeleteCallback: not implemented
    """
    #super().__init__(viewer, layer)
    super().__init__()

    self._viewer = viewer
    self._layer = layer

    # for label layer, this is an integer (not a set)
    self._selected_label = self._layer.selected_label

    # just show one selected label (hide all others)
    #self._layer.show_selected_label = True

    self._connectLayer()

selectItems(selectedRowSet)

Set the selected items in layer.

Called from _my_widget on user selectnig row(s) in table

Source code in src/napari_layer_table/_my_layer.py
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
def selectItems(self, selectedRowSet : set):
    """Set the selected items in layer.

    Called from _my_widget on user selectnig row(s) in table
    """
    #self._layer.selected_data = selectedRowSet
    selectedRowList = list(selectedRowSet)
    if selectedRowList:
        selectedRow = selectedRowList[0]
        logger.info(f'labelLayer selectedRow: {selectedRow}')
        self._layer.selected_label = selectedRow

slot_selected_label(event)

Respond to user setting label in viewer.

Source code in src/napari_layer_table/_my_layer.py
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
def slot_selected_label(self, event):
    """Respond to user setting label in viewer.
    """
    logger.info('labelLayer')
    selected_label = self._layer.selected_label # int

    if selected_label == self._selected_label:
        print('  no new label selection')
        return

    self._selected_label = selected_label
    print('  _selected_label:', self._selected_label)

    #properties = self.getDataFrame()

    #print('  properties:')
    #pprint(properties)

    '''
    print('  event.source', event.source)  # same as self._layer
    print('  event.type', event.type)
    print('  selected_label:', self._layer.selected_label)
    print('  event.source.selected_label:', event.source.selected_label)
    '''

    self._selected_label = event.source._selected_label  # int

    properties = self.getDataFrame()
    print('  -->> emit "select"')
    # in label layer we will only every select one label
    # signal/slot expects a list
    selectedLabelList = [self._selected_label]
    emptyDict = dict()
    self.signalDataChanged.emit('select', selectedLabelList, emptyDict, properties)

mmLayer

Bases: QObject

Source code in src/napari_layer_table/_my_layer.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
class mmLayer(QtCore.QObject):
    # Note: these have to be kept in sync with labelLayer signals
    signalDataChanged = QtCore.Signal(str,  # action
                            set,            # selected set
                            dict,           # _layerSelectionCopy
                            pd.DataFrame)   # properties DataFrame

    signalLayerNameChange = QtCore.Signal(str)

    def __init__(self, viewer, layer,
                    onAddCallback=None,
                    onDeleteCallback=None,
                    ):
        """
        Args:
            onAddCallback (func) params(set, pd.DataFrame) return Union[None, dict]
        """
        super().__init__()

        self._viewer = viewer
        self._layer = layer

        self._shift_click_for_new = False
        self._onAddCallback = onAddCallback  # callback to accept/reject/modify add
        self.newOnShiftClick(onAddCallback is not None)

        # not sure how to use bind_key
        self._deleteOnDeleteKey = False
        self._onDeleteCallback = onDeleteCallback  # callback to accept/reject/modify add
        self.deleteOnDeleteKey(onDeleteCallback is not None)
        #self._layer.bind_key(self.xxx)

        self._flashTimer = None
        # flash on point selection

        self._undo = None  # creat undo object in derived classes

        self._onlyOneLayer = True

        self._blockOnAdd = False

        self._layerSelectionCopy = None  # a copy of all selected layer data

        self._selected_data = layer.selected_data.copy()

        # replaced by full copy of table in _layerSelectionCopy
        '''if list(self._selected_data):
            self._selected_data2 = layer.data[self._selected_data]
        else:
            # TODO (cudmore) can we initialize with None ???
            self._selected_data2 = np.ndarray([])
        '''

        self._numItems = len(layer.data)

        self._connectLayer()

        # slots to detect a change in layer selection
        self._viewer.layers.events.inserting.connect(self.slot_insert_layer)
        self._viewer.layers.events.inserted.connect(self.slot_insert_layer)
        self._viewer.layers.events.removed.connect(self.slot_remove_layer)
        self._viewer.layers.events.removing.connect(self.slot_remove_layer)

        self._viewer.layers.selection.events.changed.connect(self.slot_select_layer)

        # todo (cudmore) for now, experiment with this in the mmPoints layer
        #self._undo = mmUndo(self)  # undo connect to self.signalDataChanged

    '''
    def keyPressEvent(self, event):
        logger.info('')
        if event.key() == QtCore.Qt.Key_Q:
            pass
        elif event.key() == QtCore.Qt.Key_Enter:
            pass
        event.accept()
    '''

    @property
    def properties(self):
        return self._layer.properties

    def old_on_delete_key_callback(self):
        """Intercept del keystroke and decide if we really want to delete.

        Notes:
            not implemented.
        """
        logger.info('')

    def _copy_data(self):
        """Make a complete copy of a layer selection.

        Implement in derived classes.
        """
        pass

    def doUndo(self):
        if self._undo is not None:
            self._undo.doUndo()

    def addFeature(self, featureName : str, columnName : Union[str, None] = None):
        """Add a feature to layer.

        Args:
            featureName: The key name of the feature to add to layer
            columnName: Specify if column name in table is different from feature name.

        Notes:
            We don't want our features (like 'x') to contaminate
            an existing layer features. User may already have a feature 'x'
            we don't want to over-wrte it.

            Thus, use
                featureName = 'ltp_x'
                columnName = 'x'
        """
        if columnName is None:
            columnName = featureName
        features = self._layer.features # get existing
        # check if column exists
        if featureName in features.columns:
            logger.warning('Feature already exists')
            return
        features[featureName] = None  # need to be len(layer)

    def snapToItem(self, selectedRow : int, isAlt : bool =False):
        """Visually snap the viewer to selected item.
        """
        pass

    def bringToFront(self):
        """Bring the underlying layer to the front.
        """
        if self._viewer.layers.selection.active != self._layer:
            self._viewer.layers.selection.active = self._layer

    def getName(self):
        """Get the name from underlying layer.
        """
        return self._layer.name

    def numItems(self):
        """Get the current number of items.

        Used to determine if we have add/delete in slot_user_edit_highlight().
        """
        return self._numItems

    def selectItems(self, selectedRowSet : set):
        """Set the selected items in layer.

        Called from _my_widget on user selectnig row(s) in table

        TODO:
            not used.
        """
        self._layer.selected_data = selectedRowSet

    @property
    def selected_data(self):
        return self._selected_data

    def _connectLayer(self, layer=None):

        self._layer.events.name.connect(self.slot_user_edit_name)
        self._layer.events.highlight.connect(self.slot_user_edit_highlight)
        self._layer.events.data.connect(self.slot_user_edit_data)

        # no longer available in PyPi napari
        # self._layer.events.features.connect(self.slot_user_edit_features)

        # we want to receive an event when user sets the face color of (points, shapes)
        # this does not trigger?
        self._layer.events.face_color.connect(self.slot_user_edit_face_color)
        # this triggers but only for points layer
        #self._layer._face.events.current_color.connect(self.slot_user_edit_face_color)

        # want to use this, only triggers for shapes layer
        #self._layer.events.current_face_color.connect(self.slot_user_edit_face_color)

        # this is never called, not sure if it even triggers
        self._layer.events.properties.connect(self.slot_user_edit_properties)

    def newOnShiftClick(self, on = None):
        """Enable/disable shift+click for new points.
        """
        if on is not None:
            self._shift_click_for_new = on

        if self._shift_click_for_new:
            logger.info(f'{self._derivedClassName()} enabling newOnShiftClick')
            self._layer.mouse_drag_callbacks.append(self._on_mouse_drag)
        else:
            try:
                logger.info(f'{self._derivedClassName()} disabling newOnShiftClick')
                self._layer.mouse_drag_callbacks.remove(self._on_mouse_drag)
            except (ValueError) as e:
                # not in list
                pass

    def deleteOnDeleteKey(self, on = None):
        self._deleteOnDeleteKey = on

    def _on_mouse_drag(self, layer, event):
        """Handle user mouse-clicks. Intercept shift+click to make a new point.

        Will only be called when install with newOnShiftClick().
        """
        if 'Shift' in event.modifiers:
            # make a new point at cursor position
            onAddReturn = {}
            if self._onAddCallback is not None:
                logger.info(f'checking with _onAddCallback ...')
                # onAddCallback should determine (i) if we want to actually add
                # (ii) if add is ok, return a dict of values for selected row
                onAddReturn = self._onAddCallback(self._selected_data, self.getDataFrame())
                if onAddReturn is None:
                    print('    shift+clik was rejected -->> no new point')
                    return
                else:
                    print('  on add return returned dict:')
                    pprint(onAddReturn)

            data_coordinates = self._layer.world_to_data(event.position)
            # always add as integer pixels (not fractional/float pixels)
            cords = np.round(data_coordinates).astype(int)

            # add to layer, only for points layer?
            # for shape layer type 'path', use add_paths()
            # self._layer.add(cords)
            self.addAnnotation(cords, event, features=onAddReturn)

            # set features from onAddReturn

    '''
    def on_mouse_wheel(self, layer, event):
        """Mouse wheel callback.

        Over-ride default behavior to become

            mouse-wheel: scroll through image planes (need a 3d image)
            mouse-wheel + ctrl: zoom in/out on mouse position
        """        
        # used to find what data the event has
        pprint(vars(event))

        isShift = 'Shift' in event.modifiers
        isControl = 'Control' in event.modifiers

        #xDelta = event.delta[0]  # ignore
        yDelta = event.delta[1]  # +1 is 'up', -1 is 'down'

        logger.info(f'handled:{event.handled} isShift:{isShift} isControl:{isControl} yDelta:{yDelta}')

        #self.dims._increment_dims_left()
        if isControl:            
            zoomScale = 0.1
            _start_zoom = self._viewer.camera.zoom
            self._viewer.camera.zoom = _start_zoom * (zoomScale + yDelta)
            #event.handled = True
    '''

    def addAnnotation(self, coords, event = None, features:dict = None):
        """Add an annotation to a layer.

        Define when deriving. For points layer use 'self._layer.add(cords)'

        Args:
            coords:
            event: napari mouse down event
        """
        pass

    def getDataFrame(self, getFull=False) -> pd.DataFrame:
        """Get a dataframe from layer.

        Args:
            getFull: get full dataframe
                otherwise, get datafram for _selectedData
        """

        # self._layer.features gives us a (features, properties) pandas dataframe !!!        
        # can be empty !!!
        dfFeatures = self._layer.features  # all features

        # not neccessary, alreay a pd.DataFrame (I think)
        # dfFeatures = features_to_pandas_dataframe(dfFeatures)

        if getFull:
            selectedList = list(range(len(self._layer.data)))
        else:
            selectedList = list(self._selected_data)

        # reduce by selection
        df = dfFeatures.loc[selectedList]

        if len(df) == 0:
            df = pd.DataFrame(index=selectedList)

        #
        # This is impossibly hard to get working
        # I just want to assign a column named 'Face Color'
        # Bottom line, all cells in a pd.DataFrame need to be scalars
        # TODO (cudmore) use hex values rather than rgba

        # SettingWithCopyWarning
        # A value is trying to be set on a copy of a slice from a DataFrame.
        # Try using .loc[row_indexer,col_indexer] = value instead
        #print('=== SettingWithCopyWarning:', self._derivedClassName())
        #print('selectedList:', selectedList)
        #pprint(df)
        # ValueError: cannot set a frame with no defined index and a scalar
        # df.loc[selectedList, 'Face Color'] = ''


        if selectedList:
            # TODO (cudmore) rgb_to_hex() returns an np.array with  dtype of unicode '|U9''
            #    we want it as a string ???
            tmpColor = [str(rgb_to_hex(oneColor)[0])
                        for oneColor in self._layer.face_color[selectedList]]        

            df.loc[selectedList, 'Face Color'] = tmpColor

        return df

    def _derivedClassName(self):
        return self.__class__.__name__

    def slot_user_edit_highlight(self, event):
        """Called repeatedly on mouse hover.

        Error:
            mm_env/lib/python3.9/site-packages/numpy/core/fromnumeric.py:43:
            VisibleDeprecationWarning:
            Creating an ndarray from ragged nested sequences
            (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes)
            is deprecated.
            If you meant to do this, you must specify 'dtype=object' when creating the ndarray.!

        """
        newSelection = not setsAreEqual(event.source.selected_data, self._selected_data)

        action = 'none'
        if len(event.source.data) > self.numItems():
            # add an item: for points layer is point, for shapes layer is shape
            # event.source.selected_data gives us the added points
            # for shape layer, *this is called multiple times without the added items selected
            if newSelection:
                action = 'add'
            else:
                # this happens on add in shapes layer
                # for shapes, need to add
                print('    ==== data length changed but selection did not...')
                print('     event.source.selected_data:', event.source.selected_data)
                print('     self._selected_data:', self._selected_data)
                '''
                _newSelectionStart = self.numItems()
                _newSelectionStop = len(event.source.data)
                _newSelectionRange = np.arange(_newSelectionStart, _newSelectionStop)
                event.source.selected_data = set(_newSelectionRange)
                print(f'     tweeked event.source.selected_data: {event.source.selected_data}')
                action = 'add'
                '''
        elif len(event.source.data) < self.numItems():
            # event.source.selected_data tells us the rows
            # THEY NO LONGER EXIST
            # our current self._selected_data tells us the rows
            action = 'delete'
        elif newSelection:
            action = 'select'

        '''
        if newSelection and self._blockOnAdd and not event.source.selected_data:
            # after shapes add, we re-enter here but event.source.selected_data is empty set()
            # should be the newly added shape(s), e.g. self._selected_data
            print('        ================')
            print('        action:', action)
            print('        convert to action: select')
            event.source.selected_data = self._selected_data
            self._blockOnAdd = False
        '''

        if action != 'none':
            logger.info(f'{self._derivedClassName()}')
            print('    newSelection:', newSelection)
            print('    event.source.selected_data:', event.source.selected_data)
            print('    self._selected_data:', self._selected_data)
            print('    len(event.source.data):', len(event.source.data))
            print('    self.numItems():', self.numItems())
            print('    action:', action)

        # signal what changed
        if action == 'add':
            # for shapes layer, when we get called again selected_data == set()
            self._blockOnAdd = True

            # on add we have new items and they are selected
            self._selected_data = event.source.selected_data.copy()
            self._numItems = len(event.source.data)

            # trying to figure out shapes layer
            # after add shapes layer trigger selection with set(), not with what was added
            #if not self._selected_data:
            #    print(f'    ERROR in {self._derivedClassName()} ... new shapes are not selected')
            _selected_data_set = set(self._selected_data)  # abb 202402
            self._copy_data()  # copy all selected points to _layerSelectionCopy
            self._updateFeatures(self._selected_data)
            dfFeatures = self.getDataFrame()
            print(f'  -->> signalDataChanged.emit "add" with _selected_data:{self._selected_data}')
            self.signalDataChanged.emit('add',
                                _selected_data_set,
                                # self._selected_data,
                                self._layerSelectionCopy,
                                dfFeatures)

        elif action == 'delete':
            # on delete, data indices were deleted_selected_data
            delete_selected_data = self._selected_data.copy()
            delete_selected_data_set = set(delete_selected_data)  # abb 202402
            self._selected_data = set()
            self._numItems = len(event.source.data)

            # here we are reusing previous _layerSelectionCopy
            # from action ('add', 'select')
            logger.info(f'  -->> signalDataChanged.emit "delete" with delete_selected_data:{delete_selected_data}')
            self.signalDataChanged.emit('delete',
                            delete_selected_data_set,  # abb 202402
                            # delete_selected_data,
                            self._layerSelectionCopy,
                            pd.DataFrame())

        elif action == 'select':
            self._selected_data = event.source.selected_data.copy()

            selectedDataSet = set(self._selected_data)
            self._copy_data()  # copy all selected points to _layerSelectionCopy
            dfProperties = self.getDataFrame()

            logger.info(f'action:{action}')
            print(f'  -->> signalDataChanged.emit "select" with _selected_data:{self._selected_data}')
            pprint(dfProperties)
            print('')
            self.signalDataChanged.emit('select',
                                selectedDataSet,  # abb 202402
                                # self._selected_data,
                                self._layerSelectionCopy,
                                dfProperties)

    def slot_user_edit_data(self, event):
        """User edited a point in the current layer.

        This is generated when user finishes a click+drag.

        Notes:
            On key-press (like delete), we need to ignore event.source.mode
        """

        # if there is no selection, there is never a change
        # this does not work for shapes layer
        if not self._selected_data:
            # no data changes when no selection
            logger.info(f'NO CHANGE BECAUSE _selected_data is (empty) {self._selected_data}')
            return

        # we usually show x/y/z in table
        # update our internal fatures
        self._updateFeatures(self._selected_data)

        # copy the selection
        self._copy_data()

        dfFeatures = self.getDataFrame()

        logger.info('')
        logger.info(f'  -->> signalDataChanged.emit "change" with _selected_data:{self._selected_data}')
        logger.info('    features:')
        pprint(dfFeatures)

        selectedDataSet = set(self._selected_data)
        self.signalDataChanged.emit('change', 
                        selectedDataSet,  # abb 202402
                        # self._selected_data, 
                        self._layerSelectionCopy, 
                        dfFeatures)

    def slot_user_edit_face_color(self, event):
        """User selected a face color.

        Change the face color of selected points.

        Note:
            - Using hex #RRGGBBAA
            - QColor takes #AARRGGBB, see _data_model.data (QtCore.Qt.ForegroundRole)
        """
        layer = self._viewer.layers.selection.active  # can be None
        try:
            print('        layer selected_data:', layer.selected_data)
            print('        self.selected_data:', self._selected_data)
            if not setsAreEqual(layer.selected_data, self._selected_data):
                logger.warning('ignoring event: selected_data do not match')
                return
        except (AttributeError) as e:
            logger.warning(e)
            return

        if self._selected_data:

            current_face_color = self._layer.current_face_color  # hex

            logger.info(f'current_face_color:{current_face_color}')

            dfProperties = self.getDataFrame()

            index = list(self._selected_data)
            dfProperties.loc[index, 'Face Color'] = current_face_color

            #for oneRowIndex in index:
            #    properties.loc[oneRowIndex, 'Face Color'] = current_face_color

            # copy selected data, not sure this is needed, updates _layerSelectionCopy
            self._copy_data()

            print('  -->> emit "change"')
            print('        self._selected_data:', self._selected_data)
            print('        dfProperties:')
            pprint(dfProperties)

            #pprint(vars(event))
            #print('\n\n')
            _selected_data_set = set(self._selected_data)
            self.signalDataChanged.emit('change',
                            _selected_data_set,
                            # self._selected_data,
                            self._layerSelectionCopy,
                            dfProperties)

    def slot_user_edit_name(self, event):
        print('slot_user_edit_name()')
        #newName = self._layer.name
        newName = event.source.name
        self.signalLayerNameChange.emit(newName)

    def slot_user_edit_properties(self, event):
        logger.info('')

    def slot_user_edit_features(self, event):
        logger.info('')

    def slot_select_layer(self, event):
        """Respond to layer selection in viewer.

        Args:
            event (Event): event.type == 'changed'
        """
        #logger.info(f'event.type: {event.type}')

        if self._onlyOneLayer:
            return

        # BUG: does not give the correct layer
        # Need to query global viewer. Is selected layer in event???
        #layer = event.source
        layer = self._viewer.layers.selection.active

        #if layer is not None:
        #    if layer != self._layer:
        #        self.connectLayer(layer)
        if layer != self._layer:
            self._connectLayer(layer)

    def slot_insert_layer(self, event):
        """Respond to new layer in viewer.
        """

        if self._onlyOneLayer:
            return

        if event.type == 'inserting':
            pass
        elif event.type == 'inserted':
            logger.info(f'New layer "{event.value}" was inserted at index {event.index}')

            layer = event.value
            self._connectLayer(layer)

    def slot_remove_layer(self, event):
        """Respond to layer delete in viewer.
        """

        if self._onlyOneLayer:
            return

        if event.type == 'removing':
            pass
        elif event.type == 'removed':
            logger.info(f'Removed layer "{event.value}"')

            # table data is empty
            #self.refreshTableData([])

            # TODO: does not work, newSelectedLayer is always None
            # we are not receiving new layer selection
            # do it manually from current state of the viewer
            newSelectedLayer = self._viewer.layers.selection.active
            self._connectLayer(newSelectedLayer)

    def printEvent(self, event):
        """Print all info on an event.

        TODO (cudmore) Not used.
        """
        print(f'    _printEvent() type:{type(event)}')
        print(f'    event.type: {event.type}')
        print(f'    event.source: {event.source} {type(event.source)}')
        print(f'    event.source.name: "{event.source.name}"')        
        print(f'    event.source.mode: {event.source.mode}')        
        print(f'    event.source.selected_data: {event.source.selected_data}')

        # data is either a list or an ndarray
        print(f'    type(event.source.data): {type(event.source.data)}')
        print(f'    len(event.source.data): {len(event.source.data)}')

        print(f'    event.source.data: {event.source.data}')

__init__(viewer, layer, onAddCallback=None, onDeleteCallback=None)

Source code in src/napari_layer_table/_my_layer.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def __init__(self, viewer, layer,
                onAddCallback=None,
                onDeleteCallback=None,
                ):
    """
    Args:
        onAddCallback (func) params(set, pd.DataFrame) return Union[None, dict]
    """
    super().__init__()

    self._viewer = viewer
    self._layer = layer

    self._shift_click_for_new = False
    self._onAddCallback = onAddCallback  # callback to accept/reject/modify add
    self.newOnShiftClick(onAddCallback is not None)

    # not sure how to use bind_key
    self._deleteOnDeleteKey = False
    self._onDeleteCallback = onDeleteCallback  # callback to accept/reject/modify add
    self.deleteOnDeleteKey(onDeleteCallback is not None)
    #self._layer.bind_key(self.xxx)

    self._flashTimer = None
    # flash on point selection

    self._undo = None  # creat undo object in derived classes

    self._onlyOneLayer = True

    self._blockOnAdd = False

    self._layerSelectionCopy = None  # a copy of all selected layer data

    self._selected_data = layer.selected_data.copy()

    # replaced by full copy of table in _layerSelectionCopy
    '''if list(self._selected_data):
        self._selected_data2 = layer.data[self._selected_data]
    else:
        # TODO (cudmore) can we initialize with None ???
        self._selected_data2 = np.ndarray([])
    '''

    self._numItems = len(layer.data)

    self._connectLayer()

    # slots to detect a change in layer selection
    self._viewer.layers.events.inserting.connect(self.slot_insert_layer)
    self._viewer.layers.events.inserted.connect(self.slot_insert_layer)
    self._viewer.layers.events.removed.connect(self.slot_remove_layer)
    self._viewer.layers.events.removing.connect(self.slot_remove_layer)

    self._viewer.layers.selection.events.changed.connect(self.slot_select_layer)

addAnnotation(coords, event=None, features=None)

Add an annotation to a layer.

Define when deriving. For points layer use 'self._layer.add(cords)'

Parameters:

Name Type Description Default
coords
required
event

napari mouse down event

None
Source code in src/napari_layer_table/_my_layer.py
339
340
341
342
343
344
345
346
347
348
def addAnnotation(self, coords, event = None, features:dict = None):
    """Add an annotation to a layer.

    Define when deriving. For points layer use 'self._layer.add(cords)'

    Args:
        coords:
        event: napari mouse down event
    """
    pass

addFeature(featureName, columnName=None)

Add a feature to layer.

Parameters:

Name Type Description Default
featureName str

The key name of the feature to add to layer

required
columnName Union[str, None]

Specify if column name in table is different from feature name.

None
Notes

We don't want our features (like 'x') to contaminate an existing layer features. User may already have a feature 'x' we don't want to over-wrte it.

Thus, use featureName = 'ltp_x' columnName = 'x'

Source code in src/napari_layer_table/_my_layer.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def addFeature(self, featureName : str, columnName : Union[str, None] = None):
    """Add a feature to layer.

    Args:
        featureName: The key name of the feature to add to layer
        columnName: Specify if column name in table is different from feature name.

    Notes:
        We don't want our features (like 'x') to contaminate
        an existing layer features. User may already have a feature 'x'
        we don't want to over-wrte it.

        Thus, use
            featureName = 'ltp_x'
            columnName = 'x'
    """
    if columnName is None:
        columnName = featureName
    features = self._layer.features # get existing
    # check if column exists
    if featureName in features.columns:
        logger.warning('Feature already exists')
        return
    features[featureName] = None  # need to be len(layer)

bringToFront()

Bring the underlying layer to the front.

Source code in src/napari_layer_table/_my_layer.py
207
208
209
210
211
def bringToFront(self):
    """Bring the underlying layer to the front.
    """
    if self._viewer.layers.selection.active != self._layer:
        self._viewer.layers.selection.active = self._layer

getDataFrame(getFull=False)

Get a dataframe from layer.

Parameters:

Name Type Description Default
getFull

get full dataframe otherwise, get datafram for _selectedData

False
Source code in src/napari_layer_table/_my_layer.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
def getDataFrame(self, getFull=False) -> pd.DataFrame:
    """Get a dataframe from layer.

    Args:
        getFull: get full dataframe
            otherwise, get datafram for _selectedData
    """

    # self._layer.features gives us a (features, properties) pandas dataframe !!!        
    # can be empty !!!
    dfFeatures = self._layer.features  # all features

    # not neccessary, alreay a pd.DataFrame (I think)
    # dfFeatures = features_to_pandas_dataframe(dfFeatures)

    if getFull:
        selectedList = list(range(len(self._layer.data)))
    else:
        selectedList = list(self._selected_data)

    # reduce by selection
    df = dfFeatures.loc[selectedList]

    if len(df) == 0:
        df = pd.DataFrame(index=selectedList)

    #
    # This is impossibly hard to get working
    # I just want to assign a column named 'Face Color'
    # Bottom line, all cells in a pd.DataFrame need to be scalars
    # TODO (cudmore) use hex values rather than rgba

    # SettingWithCopyWarning
    # A value is trying to be set on a copy of a slice from a DataFrame.
    # Try using .loc[row_indexer,col_indexer] = value instead
    #print('=== SettingWithCopyWarning:', self._derivedClassName())
    #print('selectedList:', selectedList)
    #pprint(df)
    # ValueError: cannot set a frame with no defined index and a scalar
    # df.loc[selectedList, 'Face Color'] = ''


    if selectedList:
        # TODO (cudmore) rgb_to_hex() returns an np.array with  dtype of unicode '|U9''
        #    we want it as a string ???
        tmpColor = [str(rgb_to_hex(oneColor)[0])
                    for oneColor in self._layer.face_color[selectedList]]        

        df.loc[selectedList, 'Face Color'] = tmpColor

    return df

getName()

Get the name from underlying layer.

Source code in src/napari_layer_table/_my_layer.py
213
214
215
216
def getName(self):
    """Get the name from underlying layer.
    """
    return self._layer.name

newOnShiftClick(on=None)

Enable/disable shift+click for new points.

Source code in src/napari_layer_table/_my_layer.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def newOnShiftClick(self, on = None):
    """Enable/disable shift+click for new points.
    """
    if on is not None:
        self._shift_click_for_new = on

    if self._shift_click_for_new:
        logger.info(f'{self._derivedClassName()} enabling newOnShiftClick')
        self._layer.mouse_drag_callbacks.append(self._on_mouse_drag)
    else:
        try:
            logger.info(f'{self._derivedClassName()} disabling newOnShiftClick')
            self._layer.mouse_drag_callbacks.remove(self._on_mouse_drag)
        except (ValueError) as e:
            # not in list
            pass

numItems()

Get the current number of items.

Used to determine if we have add/delete in slot_user_edit_highlight().

Source code in src/napari_layer_table/_my_layer.py
218
219
220
221
222
223
def numItems(self):
    """Get the current number of items.

    Used to determine if we have add/delete in slot_user_edit_highlight().
    """
    return self._numItems

old_on_delete_key_callback()

Intercept del keystroke and decide if we really want to delete.

Notes

not implemented.

Source code in src/napari_layer_table/_my_layer.py
158
159
160
161
162
163
164
def old_on_delete_key_callback(self):
    """Intercept del keystroke and decide if we really want to delete.

    Notes:
        not implemented.
    """
    logger.info('')

printEvent(event)

Print all info on an event.

TODO (cudmore) Not used.

Source code in src/napari_layer_table/_my_layer.py
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
def printEvent(self, event):
    """Print all info on an event.

    TODO (cudmore) Not used.
    """
    print(f'    _printEvent() type:{type(event)}')
    print(f'    event.type: {event.type}')
    print(f'    event.source: {event.source} {type(event.source)}')
    print(f'    event.source.name: "{event.source.name}"')        
    print(f'    event.source.mode: {event.source.mode}')        
    print(f'    event.source.selected_data: {event.source.selected_data}')

    # data is either a list or an ndarray
    print(f'    type(event.source.data): {type(event.source.data)}')
    print(f'    len(event.source.data): {len(event.source.data)}')

    print(f'    event.source.data: {event.source.data}')

selectItems(selectedRowSet)

Set the selected items in layer.

Called from _my_widget on user selectnig row(s) in table

TODO

not used.

Source code in src/napari_layer_table/_my_layer.py
225
226
227
228
229
230
231
232
233
def selectItems(self, selectedRowSet : set):
    """Set the selected items in layer.

    Called from _my_widget on user selectnig row(s) in table

    TODO:
        not used.
    """
    self._layer.selected_data = selectedRowSet

slot_insert_layer(event)

Respond to new layer in viewer.

Source code in src/napari_layer_table/_my_layer.py
647
648
649
650
651
652
653
654
655
656
657
658
659
660
def slot_insert_layer(self, event):
    """Respond to new layer in viewer.
    """

    if self._onlyOneLayer:
        return

    if event.type == 'inserting':
        pass
    elif event.type == 'inserted':
        logger.info(f'New layer "{event.value}" was inserted at index {event.index}')

        layer = event.value
        self._connectLayer(layer)

slot_remove_layer(event)

Respond to layer delete in viewer.

Source code in src/napari_layer_table/_my_layer.py
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
def slot_remove_layer(self, event):
    """Respond to layer delete in viewer.
    """

    if self._onlyOneLayer:
        return

    if event.type == 'removing':
        pass
    elif event.type == 'removed':
        logger.info(f'Removed layer "{event.value}"')

        # table data is empty
        #self.refreshTableData([])

        # TODO: does not work, newSelectedLayer is always None
        # we are not receiving new layer selection
        # do it manually from current state of the viewer
        newSelectedLayer = self._viewer.layers.selection.active
        self._connectLayer(newSelectedLayer)

slot_select_layer(event)

Respond to layer selection in viewer.

Parameters:

Name Type Description Default
event Event

event.type == 'changed'

required
Source code in src/napari_layer_table/_my_layer.py
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
def slot_select_layer(self, event):
    """Respond to layer selection in viewer.

    Args:
        event (Event): event.type == 'changed'
    """
    #logger.info(f'event.type: {event.type}')

    if self._onlyOneLayer:
        return

    # BUG: does not give the correct layer
    # Need to query global viewer. Is selected layer in event???
    #layer = event.source
    layer = self._viewer.layers.selection.active

    #if layer is not None:
    #    if layer != self._layer:
    #        self.connectLayer(layer)
    if layer != self._layer:
        self._connectLayer(layer)

slot_user_edit_data(event)

User edited a point in the current layer.

This is generated when user finishes a click+drag.

Notes

On key-press (like delete), we need to ignore event.source.mode

Source code in src/napari_layer_table/_my_layer.py
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def slot_user_edit_data(self, event):
    """User edited a point in the current layer.

    This is generated when user finishes a click+drag.

    Notes:
        On key-press (like delete), we need to ignore event.source.mode
    """

    # if there is no selection, there is never a change
    # this does not work for shapes layer
    if not self._selected_data:
        # no data changes when no selection
        logger.info(f'NO CHANGE BECAUSE _selected_data is (empty) {self._selected_data}')
        return

    # we usually show x/y/z in table
    # update our internal fatures
    self._updateFeatures(self._selected_data)

    # copy the selection
    self._copy_data()

    dfFeatures = self.getDataFrame()

    logger.info('')
    logger.info(f'  -->> signalDataChanged.emit "change" with _selected_data:{self._selected_data}')
    logger.info('    features:')
    pprint(dfFeatures)

    selectedDataSet = set(self._selected_data)
    self.signalDataChanged.emit('change', 
                    selectedDataSet,  # abb 202402
                    # self._selected_data, 
                    self._layerSelectionCopy, 
                    dfFeatures)

slot_user_edit_face_color(event)

User selected a face color.

Change the face color of selected points.

Note
  • Using hex #RRGGBBAA
  • QColor takes #AARRGGBB, see _data_model.data (QtCore.Qt.ForegroundRole)
Source code in src/napari_layer_table/_my_layer.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
def slot_user_edit_face_color(self, event):
    """User selected a face color.

    Change the face color of selected points.

    Note:
        - Using hex #RRGGBBAA
        - QColor takes #AARRGGBB, see _data_model.data (QtCore.Qt.ForegroundRole)
    """
    layer = self._viewer.layers.selection.active  # can be None
    try:
        print('        layer selected_data:', layer.selected_data)
        print('        self.selected_data:', self._selected_data)
        if not setsAreEqual(layer.selected_data, self._selected_data):
            logger.warning('ignoring event: selected_data do not match')
            return
    except (AttributeError) as e:
        logger.warning(e)
        return

    if self._selected_data:

        current_face_color = self._layer.current_face_color  # hex

        logger.info(f'current_face_color:{current_face_color}')

        dfProperties = self.getDataFrame()

        index = list(self._selected_data)
        dfProperties.loc[index, 'Face Color'] = current_face_color

        #for oneRowIndex in index:
        #    properties.loc[oneRowIndex, 'Face Color'] = current_face_color

        # copy selected data, not sure this is needed, updates _layerSelectionCopy
        self._copy_data()

        print('  -->> emit "change"')
        print('        self._selected_data:', self._selected_data)
        print('        dfProperties:')
        pprint(dfProperties)

        #pprint(vars(event))
        #print('\n\n')
        _selected_data_set = set(self._selected_data)
        self.signalDataChanged.emit('change',
                        _selected_data_set,
                        # self._selected_data,
                        self._layerSelectionCopy,
                        dfProperties)

slot_user_edit_highlight(event)

Called repeatedly on mouse hover.

Error

mm_env/lib/python3.9/site-packages/numpy/core/fromnumeric.py:43: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.!

Source code in src/napari_layer_table/_my_layer.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def slot_user_edit_highlight(self, event):
    """Called repeatedly on mouse hover.

    Error:
        mm_env/lib/python3.9/site-packages/numpy/core/fromnumeric.py:43:
        VisibleDeprecationWarning:
        Creating an ndarray from ragged nested sequences
        (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes)
        is deprecated.
        If you meant to do this, you must specify 'dtype=object' when creating the ndarray.!

    """
    newSelection = not setsAreEqual(event.source.selected_data, self._selected_data)

    action = 'none'
    if len(event.source.data) > self.numItems():
        # add an item: for points layer is point, for shapes layer is shape
        # event.source.selected_data gives us the added points
        # for shape layer, *this is called multiple times without the added items selected
        if newSelection:
            action = 'add'
        else:
            # this happens on add in shapes layer
            # for shapes, need to add
            print('    ==== data length changed but selection did not...')
            print('     event.source.selected_data:', event.source.selected_data)
            print('     self._selected_data:', self._selected_data)
            '''
            _newSelectionStart = self.numItems()
            _newSelectionStop = len(event.source.data)
            _newSelectionRange = np.arange(_newSelectionStart, _newSelectionStop)
            event.source.selected_data = set(_newSelectionRange)
            print(f'     tweeked event.source.selected_data: {event.source.selected_data}')
            action = 'add'
            '''
    elif len(event.source.data) < self.numItems():
        # event.source.selected_data tells us the rows
        # THEY NO LONGER EXIST
        # our current self._selected_data tells us the rows
        action = 'delete'
    elif newSelection:
        action = 'select'

    '''
    if newSelection and self._blockOnAdd and not event.source.selected_data:
        # after shapes add, we re-enter here but event.source.selected_data is empty set()
        # should be the newly added shape(s), e.g. self._selected_data
        print('        ================')
        print('        action:', action)
        print('        convert to action: select')
        event.source.selected_data = self._selected_data
        self._blockOnAdd = False
    '''

    if action != 'none':
        logger.info(f'{self._derivedClassName()}')
        print('    newSelection:', newSelection)
        print('    event.source.selected_data:', event.source.selected_data)
        print('    self._selected_data:', self._selected_data)
        print('    len(event.source.data):', len(event.source.data))
        print('    self.numItems():', self.numItems())
        print('    action:', action)

    # signal what changed
    if action == 'add':
        # for shapes layer, when we get called again selected_data == set()
        self._blockOnAdd = True

        # on add we have new items and they are selected
        self._selected_data = event.source.selected_data.copy()
        self._numItems = len(event.source.data)

        # trying to figure out shapes layer
        # after add shapes layer trigger selection with set(), not with what was added
        #if not self._selected_data:
        #    print(f'    ERROR in {self._derivedClassName()} ... new shapes are not selected')
        _selected_data_set = set(self._selected_data)  # abb 202402
        self._copy_data()  # copy all selected points to _layerSelectionCopy
        self._updateFeatures(self._selected_data)
        dfFeatures = self.getDataFrame()
        print(f'  -->> signalDataChanged.emit "add" with _selected_data:{self._selected_data}')
        self.signalDataChanged.emit('add',
                            _selected_data_set,
                            # self._selected_data,
                            self._layerSelectionCopy,
                            dfFeatures)

    elif action == 'delete':
        # on delete, data indices were deleted_selected_data
        delete_selected_data = self._selected_data.copy()
        delete_selected_data_set = set(delete_selected_data)  # abb 202402
        self._selected_data = set()
        self._numItems = len(event.source.data)

        # here we are reusing previous _layerSelectionCopy
        # from action ('add', 'select')
        logger.info(f'  -->> signalDataChanged.emit "delete" with delete_selected_data:{delete_selected_data}')
        self.signalDataChanged.emit('delete',
                        delete_selected_data_set,  # abb 202402
                        # delete_selected_data,
                        self._layerSelectionCopy,
                        pd.DataFrame())

    elif action == 'select':
        self._selected_data = event.source.selected_data.copy()

        selectedDataSet = set(self._selected_data)
        self._copy_data()  # copy all selected points to _layerSelectionCopy
        dfProperties = self.getDataFrame()

        logger.info(f'action:{action}')
        print(f'  -->> signalDataChanged.emit "select" with _selected_data:{self._selected_data}')
        pprint(dfProperties)
        print('')
        self.signalDataChanged.emit('select',
                            selectedDataSet,  # abb 202402
                            # self._selected_data,
                            self._layerSelectionCopy,
                            dfProperties)

snapToItem(selectedRow, isAlt=False)

Visually snap the viewer to selected item.

Source code in src/napari_layer_table/_my_layer.py
202
203
204
205
def snapToItem(self, selectedRow : int, isAlt : bool =False):
    """Visually snap the viewer to selected item.
    """
    pass

pointsLayer

Bases: mmLayer

Source code in src/napari_layer_table/_my_layer.py
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
class pointsLayer(mmLayer):
    def __init__(self, viewer, layer, *args, **kwargs):

        super().__init__(viewer, layer, *args, **kwargs)

        logger.info('Creating pointsLayer')

        # features this layer will calculate
        # updated in _updateFeatures
        # stored in layer features and displayed as columns in table
        self.addFeature('x')
        self.addFeature('y')
        if self._layer.ndim >= 3:
            self.addFeature('z')

        self._updateFeatures()

        # todo (cudmore) for now, experiment with this in the mmPoints layer
        #self._undo = mmUndo(self)  # undo connect to self.signalDataChanged

    def _connectLayer(self, layer=None):
        """Connect underlying layer signals to slots.
        """
        super()._connectLayer()

        # this triggers but only for points layer
        # was this
        self._layer._face.events.current_color.connect(self.slot_user_edit_face_color)
        # this this
        #self._layer.events.face_color.connect(self.slot_user_edit_face_color)
        #self._layer.events.current_face_color.connect(self.slot_user_edit_face_color)

        self._layer.events.symbol.connect(self.slot_user_edit_symbol)  # points layer
        self._layer.events.size.connect(self.slot_user_edit_size)  # points layer

    def _updateFeatures(self, selectedDataSet=None):
        """Update layer features based on selection.

        Used for (i) creation and (ii) on data move.

        Args:
            selectedDataSet (set) selected data, Pass None to update all.
        """
        if selectedDataSet is None:
            selectedDataSet = set(range(self.numItems()))


        selectedList = list(selectedDataSet)

        # logger.info(f'check x/y/z order')
        # print(self._layer.data)

        #TODO: (cudmore) what if points layer has dim > 3 ???
        if self._layer.ndim == 3:
            self._layer.features.loc[selectedList, 'z'] = \
                            self._layer.data[selectedList,0]
            self._layer.features.loc[selectedList, 'x'] = \
                                self._layer.data[selectedList,2]
            self._layer.features.loc[selectedList, 'y'] = \
                                self._layer.data[selectedList,1]
        elif self._layer.ndim == 2:
            self._layer.features.loc[selectedList, 'x'] = \
                                self._layer.data[selectedList,1]
            self._layer.features.loc[selectedList, 'y'] = \
                                self._layer.data[selectedList,0]

    def _copy_data(self):
        """Copy selected points to clipboard.

        Taken from napari.layers.points.points.py

        This is used to capture 'state' so we can undo with _paste_data

        problems with `pip install napari`
        e.g. 'text': layer.text._copy(index)
        gives error: AttributeError: 'TextManager' object has no attribute '_copy'

        TODO (cudmore) this is changing with different version of napari.
        """
        if len(self.selected_data) > 0:
            layer = self._layer  # abb
            index = list(self.selected_data)
            self._layerSelectionCopy = {
                'data': deepcopy(layer.data[index]),
                'edge_color': deepcopy(layer.edge_color[index]),
                'face_color': deepcopy(layer.face_color[index]),
                'shown': deepcopy(layer.shown[index]),
                'size': deepcopy(layer.size[index]),
                'edge_width': deepcopy(layer.edge_width[index]),
                'features': deepcopy(layer.features.iloc[index]),
                # 20240612
                # AttributeError: 'Points' object has no attribute '_slice_indices'
                # 'indices': layer._slice_indices,

                #'text': layer.text._copy(index),
            }
            # TODO (Cudmore) layer.text.values is usually a <class 'numpy.ndarray'>
            # is this always true?
            # secondly, what is layer.text.value anyway? and what is dtype <U1
            # print(f'  === layer.text.values: "{layer.text.values}" {type(layer.text.values)}')
            # print('    ', layer.text.values.shape, layer.text.values.dtype)
            #if len(layer.text.values.shape) == 0:
            if layer.text.values.size == 0:
                    self._layerSelectionCopy['text'] = np.empty(0)
            else:
                try:
                    self._layerSelectionCopy['text'] = deepcopy(layer.text.values[index])
                except (IndexError) as e:
                    logger.error('   I DO NOT UNDERSTAND HOW TO FIX THIS!')
                    logger.error(e)
                    self._layerSelectionCopy['text'] = np.empty(0)

        else:
            self._layerSelectionCopy = {}

    def _paste_data(self, layerSelectionCopy=None):
        """Paste any point from clipboard and select them.

        Used by undo to 'paste/add' after delete.

        Copy of code in napari.layers.points.points.py

        We need to swap self ... for `layer = self._layer``

        Notes:
            This is very complicated, will break on napari updates.
            Hard to unit test.
        """
        layer = self._layer
        if layerSelectionCopy is None:
            _clipboard = self._layerSelectionCopy
        else:
            _clipboard = layerSelectionCopy

        npoints = len(layer._view_data)
        totpoints = len(layer.data)

        #if len(layer._clipboard.keys()) > 0:
        if len(_clipboard.keys()) > 0:
            not_disp = layer._dims_not_displayed
            data = deepcopy(_clipboard['data'])
            offset = [
                layer._slice_indices[i] - _clipboard['indices'][i]
                for i in not_disp
            ]
            data[:, not_disp] = data[:, not_disp] + np.array(offset)
            layer._data = np.append(layer.data, data, axis=0)
            layer._shown = np.append(
                layer.shown, deepcopy(_clipboard['shown']), axis=0
            )
            layer._size = np.append(
                layer.size, deepcopy(_clipboard['size']), axis=0
            )

            #layer._feature_table.append(_clipboard['features'])

            #layer.text._paste(**_clipboard['text'])

            layer._edge_width = np.append(
                layer.edge_width,
                deepcopy(_clipboard['edge_width']),
                axis=0,
            )
            layer._edge._paste(
                colors=_clipboard['edge_color'],
                properties=_features_to_properties(
                    _clipboard['features']
                ),
            )
            layer._face._paste(
                colors=_clipboard['face_color'],
                properties=_features_to_properties(
                    _clipboard['features']
                ),
            )

            # new in `pip install napari`
            layer._feature_table.append(_clipboard['features'])

            layer._selected_view = list(
                range(npoints, npoints + len(_clipboard['data']))
            )
            layer._selected_data = set(
                range(totpoints, totpoints + len(_clipboard['data']))
            )

            if len(_clipboard['text']) > 0:
                layer.text.values = np.concatenate(
                    (layer.text.values, _clipboard['text']), axis=0
                )

            layer.refresh()

    def getDataFrame(self, getFull=False) -> pd.DataFrame:
        # getDataFrame
        # TODO (cudmore) add symbol encoding

        df = super().getDataFrame(getFull=getFull)

        if getFull:
            selectedList = list(range(len(self._layer.data)))
        else:
            selectedList = list(self._selected_data)

        # logger.warning(f'selectedList:{selectedList}')

        # now handled by _updateFeatures (only update when needed)
        '''
        # prepend (z,y,x)) columns
        df.insert(0, 'x', self._layer.data[selectedList,2])
        df.insert(0, 'y', self._layer.data[selectedList,1])
        if self._layer.ndim == 3:
            df.insert(0, 'z', self._layer.data[selectedList,0])
        '''

        # abb 202402 we are receiving a list of symbols
        # prepend symbol column
        symbol = self._layer.symbol  # str
        # logger.warning(f'getFull:{getFull} received symbol:{symbol} {type(symbol)}')
        # symbol = str(symbol)  # abb 20240206
        symbol = [SYMBOL_ALIAS[str(_symbol)] for _symbol in symbol]  # abb 202402

        # abb remove 202402
        # try:
        #     symbol = SYMBOL_ALIAS[symbol]
        # except (KeyError) as e:
        #     logger.warning(f'did not find symbol in SYMBOL_ALIAS named "{symbol}"')
        #     symbol = 'X'
        # # this is needed to keep number of rows correct
        # symbolList = [symbol] * len(selectedList)  # data.shape[0]  # make symbols for each point

        # abb 202402 cludge
        #symbolList = [symbol[0]] * len(selectedList)  # data.shape[0]  # make symbols for each point

        symbolList = [symbol[i] for i in selectedList]

        df.insert(loc=0, column='Symbol', value=symbolList)  # insert as first column
        # df.insert(loc=0, column='Symbol', value=symbol)  # insert as first column

        return df

    def addAnnotation(self, coords, event=None, features:dict = None):
        """Add a single annotation to a layer.

        Define when deriving. For points layer use 'self._layer.add(cords)'

        Notes:
            Does not seem to be a simple way to add points to existing layer.
            This does not set properties/features correctly
        """

        '''
        if event is not None:
            print('calling _points_mouse_bindings()')
            _points_mouse_bindings.add(self._layer, event)
        else:
            self._layer.add(coords)
        '''

        #
        # IMPORTANT !!!!
        #
        # do the add (napari), this TRIGGERS
        # add events before it returns
        self._layer.add(coords)  # napari function call

        # point was added and all callbacks responded

        # assign features
        logger.info('assigning features from external return dict')
        addedIdx = self._numItems  # after added
        addedIdx -= 1
        for featureColumn in self._layer.features.columns:
            if featureColumn in features.keys():
                addedFeatureValue = features[featureColumn]
                print(f'      addedIdx:{addedIdx} featureColumn:{featureColumn} addedFeatureValue:{addedFeatureValue}')
                self._layer.features.loc[addedIdx, featureColumn] = addedFeatureValue
            else:
                # _layer has a feature we did not set???
                print(f'  did not find featureColumn:{featureColumn} in added features')
                pass

    def _flashItem(self, selectedRow : int):
        """Flash size/color if selected item to make it visible to user.

        Notes:
            layer.refresh() did not do update, instead we are
            tapping into and refreshing Qt in the event loop with 
                get_app().processEvents()
        """

        # parameter
        # _numFlash = 3
        # _sleep1 = 0.07
        # _sleep2 = 0.04

        # logger.info(f'_numFlash:{_numFlash} _sleep1:{_sleep1} _sleep2:{_sleep2}')

        _origColor = self._layer.face_color[selectedRow].copy()
        _origSize = self._layer.size[selectedRow].copy()
        # _flashColor = [1., 1., 0., 1.]
        # _flashSize = _origSize / 2

        # we really need to create a timer object because
        # multiple calls will collide when they overwrite self._flashTimerIteration
        if self._flashTimer is not None and self._flashTimer.isActive():
            logger.warning(f'flash timer was still running')
            return

        self._flashTimerInterval = 30  # ms
        self._flashTimerIterations = 6  # must be even
        self._flashTimerIteration = 0

        logger.info(f'{self._flashTimerIterations} iterations at interval {self._flashTimerInterval} ms')

        self._flashTimer = QtCore.QTimer(self)
        self._flashTimer.setInterval(30)  # ms

        _origColor = self._layer.face_color[selectedRow].copy()
        _origSize = self._layer.size[selectedRow].copy()

        _callback = lambda selectedRow=selectedRow, _origColor=_origColor, _origSize=_origSize \
            : self._on_flash_timer(selectedRow, _origColor, _origSize)
        self._flashTimer.timeout.connect(_callback)
        self._flashTimer.start()

    def _on_flash_timer(self, selectedRow, _origColor, _origSize):
        """Called when self.xxx QTimer times out
        """
        doFlash = self._flashTimerIteration % 2 == 0

        # logger.info(f'    _flashTimerIteration:{self._flashTimerIteration} doFlash:{doFlash}')

        _flashColor = [1., 1., 0., 1.]
        #_flashSize = _origSize / 2
        _flashSize = _origSize * 3

        # do flash
        if doFlash:
            self._layer.face_color[selectedRow] = _flashColor
            self._layer.size[selectedRow] = _flashSize
            self._layer.refresh()
            get_app().processEvents()
            #time.sleep(_sleep1)
        else:
            self._layer.face_color[selectedRow] = _origColor
            self._layer.size[selectedRow] = _origSize
            self._layer.refresh()
            get_app().processEvents()
            #time.sleep(_sleep2)

        # increment
        self._flashTimerIteration += 1
        if self._flashTimerIteration >= self._flashTimerIterations:
            self._flashTimer.stop()

    def snapToItem(self, selectedRow : int, isAlt : bool =False):
        """Snap viewer to z-Plane of selected row and optionally to (y,x)

        Only snap when one row is selected, not multiple.

        Args:
            selectedRow (int): The row to snap to.
            isAlt (bool): If True then center point on (y,x)

        TODO:
            "Setting the camera center also resets the zoom"
            see: https://github.com/napari/napari/issues/3723
            on 20220322, was closed and should be fixed with next version of vispy
            see: https://github.com/vispy/vispy/pull/2312
        """
        isThreeD = self._layer.data.shape[1] == 3

        if isThreeD:
            zSlice = self._layer.data[selectedRow][0]  # assuming (z,y,x)
            yPnt = self._layer.data[selectedRow][1]  # assuming (z,y,x)
            xPnt = self._layer.data[selectedRow][2]  # assuming (z,y,x)
            logger.info(f'selectedRow:{selectedRow} zSlice:{zSlice} y:{yPnt} x:{xPnt}')

            # z-Plane
            axis = 0 # assuming (z,y,x)
            self._viewer.dims.set_point(axis, zSlice)

            # (y,x)
            if isAlt:
                self._viewer.camera.center = (zSlice, yPnt, xPnt)

        else:
            yPnt = self._layer.data[selectedRow][0]  # assuming (z,y,x)
            xPnt = self._layer.data[selectedRow][1]  # assuming (z,y,x)
            logger.info(f'y:{yPnt} x:{xPnt}')
            if isAlt:
                self._viewer.camera.center = (yPnt, xPnt)

        # flash selection to make it visible to user
        self._flashItem(selectedRow)

    def slot_user_edit_symbol(self, event):
        """Respond to user selecting a new symbol.

        Special case, all points in layer have same symbol, 
        need to refresh entire table.
        """

        # TODO (cudmore) add mmLayer.emitChangeAll()

        all_selected_data = set(range(self.numItems()))

        logger.info(f'-->> emit change with all_selected_data:{all_selected_data}')

        #selected_data2 = self._layer.data
        dfFeatures = self.getDataFrame(getFull=True)

        # TODO (cudmore) we do not want changeSymbol to be part of undo
        self.signalDataChanged.emit('change', all_selected_data,
                            #self._layerSelectionCopy,
                            dict(),
                            dfFeatures)

    def slot_user_edit_size(self, event):
        logger.info('  -->> NOT IMPLEMENTED')

addAnnotation(coords, event=None, features=None)

Add a single annotation to a layer.

Define when deriving. For points layer use 'self._layer.add(cords)'

Notes

Does not seem to be a simple way to add points to existing layer. This does not set properties/features correctly

Source code in src/napari_layer_table/_my_layer.py
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
def addAnnotation(self, coords, event=None, features:dict = None):
    """Add a single annotation to a layer.

    Define when deriving. For points layer use 'self._layer.add(cords)'

    Notes:
        Does not seem to be a simple way to add points to existing layer.
        This does not set properties/features correctly
    """

    '''
    if event is not None:
        print('calling _points_mouse_bindings()')
        _points_mouse_bindings.add(self._layer, event)
    else:
        self._layer.add(coords)
    '''

    #
    # IMPORTANT !!!!
    #
    # do the add (napari), this TRIGGERS
    # add events before it returns
    self._layer.add(coords)  # napari function call

    # point was added and all callbacks responded

    # assign features
    logger.info('assigning features from external return dict')
    addedIdx = self._numItems  # after added
    addedIdx -= 1
    for featureColumn in self._layer.features.columns:
        if featureColumn in features.keys():
            addedFeatureValue = features[featureColumn]
            print(f'      addedIdx:{addedIdx} featureColumn:{featureColumn} addedFeatureValue:{addedFeatureValue}')
            self._layer.features.loc[addedIdx, featureColumn] = addedFeatureValue
        else:
            # _layer has a feature we did not set???
            print(f'  did not find featureColumn:{featureColumn} in added features')
            pass

slot_user_edit_symbol(event)

Respond to user selecting a new symbol.

Special case, all points in layer have same symbol, need to refresh entire table.

Source code in src/napari_layer_table/_my_layer.py
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
def slot_user_edit_symbol(self, event):
    """Respond to user selecting a new symbol.

    Special case, all points in layer have same symbol, 
    need to refresh entire table.
    """

    # TODO (cudmore) add mmLayer.emitChangeAll()

    all_selected_data = set(range(self.numItems()))

    logger.info(f'-->> emit change with all_selected_data:{all_selected_data}')

    #selected_data2 = self._layer.data
    dfFeatures = self.getDataFrame(getFull=True)

    # TODO (cudmore) we do not want changeSymbol to be part of undo
    self.signalDataChanged.emit('change', all_selected_data,
                        #self._layerSelectionCopy,
                        dict(),
                        dfFeatures)

snapToItem(selectedRow, isAlt=False)

Snap viewer to z-Plane of selected row and optionally to (y,x)

Only snap when one row is selected, not multiple.

Parameters:

Name Type Description Default
selectedRow int

The row to snap to.

required
isAlt bool

If True then center point on (y,x)

False
TODO

"Setting the camera center also resets the zoom" see: https://github.com/napari/napari/issues/3723 on 20220322, was closed and should be fixed with next version of vispy see: https://github.com/vispy/vispy/pull/2312

Source code in src/napari_layer_table/_my_layer.py
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
def snapToItem(self, selectedRow : int, isAlt : bool =False):
    """Snap viewer to z-Plane of selected row and optionally to (y,x)

    Only snap when one row is selected, not multiple.

    Args:
        selectedRow (int): The row to snap to.
        isAlt (bool): If True then center point on (y,x)

    TODO:
        "Setting the camera center also resets the zoom"
        see: https://github.com/napari/napari/issues/3723
        on 20220322, was closed and should be fixed with next version of vispy
        see: https://github.com/vispy/vispy/pull/2312
    """
    isThreeD = self._layer.data.shape[1] == 3

    if isThreeD:
        zSlice = self._layer.data[selectedRow][0]  # assuming (z,y,x)
        yPnt = self._layer.data[selectedRow][1]  # assuming (z,y,x)
        xPnt = self._layer.data[selectedRow][2]  # assuming (z,y,x)
        logger.info(f'selectedRow:{selectedRow} zSlice:{zSlice} y:{yPnt} x:{xPnt}')

        # z-Plane
        axis = 0 # assuming (z,y,x)
        self._viewer.dims.set_point(axis, zSlice)

        # (y,x)
        if isAlt:
            self._viewer.camera.center = (zSlice, yPnt, xPnt)

    else:
        yPnt = self._layer.data[selectedRow][0]  # assuming (z,y,x)
        xPnt = self._layer.data[selectedRow][1]  # assuming (z,y,x)
        logger.info(f'y:{yPnt} x:{xPnt}')
        if isAlt:
            self._viewer.camera.center = (yPnt, xPnt)

    # flash selection to make it visible to user
    self._flashItem(selectedRow)

shapesLayer

Bases: mmLayer

event.source.mode in: 'direct': allows for shapes to be selected and their individual vertices to be moved. 'select': allows for entire shapes to be selected, moved and resized. 'VERTEX_INSERT': 'VERTEX_REMOVE':

shape_type in

'path': A list (array) of points making a path

Source code in src/napari_layer_table/_my_layer.py
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
class shapesLayer(mmLayer):
    """
    event.source.mode in:
        'direct': allows for shapes to be selected and their individual vertices to be moved.
        'select': allows for entire shapes to be selected, moved and resized.
        'VERTEX_INSERT': 
        'VERTEX_REMOVE':

    shape_type in:
        'path': A list (array) of points making a path
    """
    def __init__(self, viewer, layer, *args, **kwargs):
        super().__init__(viewer, layer, *args, **kwargs)

        self.addFeature('x')
        self.addFeature('y')
        self.addFeature('z')

        self._updateFeatures()

    def getDataFrame(self, getFull=False) -> pd.DataFrame:
        # TODO (cudmore) make sure it works for 2d/3d (what about N-Dim ???)

        df = super().getDataFrame(getFull=getFull)

        if getFull:
            selectedList = list(range(len(self._layer.data)))
        else:
            selectedList = list(self._selected_data)

        # now handled in _updateFeatures
        # iterate through each shape and calculate (z,y,x)      
        '''
        yMean = [np.mean(self._layer.data[idx][:,1]) for idx in selectedList]
        xMean = [np.mean(self._layer.data[idx][:,2]) for idx in selectedList]

        df.insert(0, 'x', xMean)
        df.insert(0, 'y', yMean)
        if self._layer.ndim == 3:
            zMean = [np.mean(self._layer.data[idx][:,0]) for idx in selectedList]
            df.insert(0, 'z', zMean)
        '''

        shape_type = [self._layer.shape_type[idx] for idx in selectedList]        
        df.insert(0, 'Shape Type', shape_type)

        return df

    def _updateFeatures(self, selectedDataSet=None):
        """Update underlying layer features based on selection.

        Used in creation and on data move.

        Args:
            selectedDataSet (set) selected data, Pass None to update all.
        """
        if selectedDataSet is None:
            selectedDataSet = set(range(self.numItems()))

        selectedList = list(selectedDataSet)

        logger.info(f'{self._derivedClassName()} selectedList:{selectedList}')
        logger.info(f'self._layer.data is:')
        print(self._layer.data)

        if self._layer.ndim == 2:
            yMean = [np.mean(self._layer.data[idx][:,0]) for idx in selectedList]
            xMean = [np.mean(self._layer.data[idx][:,1]) for idx in selectedList]

            self._layer.features.loc[selectedList, 'x'] = xMean
            self._layer.features.loc[selectedList, 'y'] = yMean

        elif self._layer.ndim >= 3:
            yMean = [np.mean(self._layer.data[idx][:,1]) for idx in selectedList]
            xMean = [np.mean(self._layer.data[idx][:,2]) for idx in selectedList]

            self._layer.features.loc[selectedList, 'x'] = xMean
            self._layer.features.loc[selectedList, 'y'] = yMean

            zMean = [np.mean(self._layer.data[idx][:,0]) for idx in selectedList]
            self._layer.features.loc[selectedList, 'z'] = zMean

        else:
            logger.warning(f'Did not update with self._layer.ndim:{self._layer.ndim}')

        print('   now self._layer.features:')
        pprint(self._layer.features)

    def _copy_data(self):
        """Copy selected shapes to clipboard.

        Taken from napari.layers.shapes.shapes.py

        This is buggy, depends on napari version !!!
        """
        if len(self.selected_data) > 0:
            layer = self._layer
            index = list(self.selected_data)
            self._layerSelectionCopy = {
                'data': [
                    deepcopy(layer._data_view.shapes[i])
                    for i in layer._selected_data
                ],
                'edge_color': deepcopy(layer._data_view._edge_color[index]),
                'face_color': deepcopy(layer._data_view._face_color[index]),
                'features': deepcopy(layer.features.iloc[index]),
                'indices': layer._slice_indices,
                'text': layer.text._copy(index),  # abb 202402 un-commented
            }

            # abb remove 202402
            # if len(layer.text.values) == 0:
            #     self._layerSelectionCopy['text'] = np.empty(0)
            # else:
            #     try:
            #         self._layerSelectionCopy['text'] = deepcopy(layer.text.values[index])
            #     except (IndexError) as e:
            #         logger.error(f'I DO NOT UNDERSTAND HOW TO FIX THIS! {e}')
            #         self._layerSelectionCopy['text'] = np.empty(0)
        else:
            self._layerSelectionCopy = {}

    def _paste_data(self, layerSelectionCopy=None):
        """Paste any shapes from clipboard and then selects them.

        Copy of code in napari.layers.shapes.shapes.py

        We need to swap self ... for `layer = self._layer``

        Notes:
            This is very complicated, will break on napari updates.
            Hard to unit test.
        """
        layer = self._layer  # replaces self.
        if layerSelectionCopy is None:
            _clipboard = self._layerSelectionCopy
        else:
            _clipboard = layerSelectionCopy

        cur_shapes = layer.nshapes
        if len(_clipboard.keys()) > 0:
            # Calculate offset based on dimension shifts
            offset = [
                layer._slice_indices[i] - _clipboard['indices'][i]
                for i in layer._dims_not_displayed
            ]

            layer._feature_table.append(_clipboard['features'])

            # Add new shape data
            for i, s in enumerate(_clipboard['data']):
                shape = deepcopy(s)
                data = copy(shape.data)
                data[:, layer._dims_not_displayed] = data[
                    :, layer._dims_not_displayed
                ] + np.array(offset)
                shape.data = data
                face_color = _clipboard['face_color'][i]
                edge_color = _clipboard['edge_color'][i]
                layer._data_view.add(
                    shape, face_color=face_color, edge_color=edge_color
                )

            if len(_clipboard['text']) > 0:
                layer.text.values = np.concatenate(
                    (layer.text.values, _clipboard['text']), axis=0
                )

            layer.selected_data = set(
                range(cur_shapes, cur_shapes + len(_clipboard['data']))
            )

            layer.move_to_front()

    def slot_user_edit_data(self, event):        
        super().slot_user_edit_data(event)
        #logger.info('shapesLayer')
        #self._printEventState(event)

    def _printEventState(self, event):
        print('    === _printEventState()')
        print('      event.source.mode:', event.source.mode)
        print('      event.source.selected_data:', event.source.selected_data)
        print('      len(event.source.data):', len(event.source.data))
        print('      self.numItems():', self.numItems())
        print('      self._selected_data:', self._selected_data)
        #print('      self._selected_data2:', self._selected_data2)

    def addShapes(self, data, shape_type):
        #if not isinstance(shape_type, list):
        #    shape_type = [shape_type]

        #print('shapeLayer.addShapes()')
        #print('  data:', type(data))
        #print('  shape_type:', shape_type)
        if shape_type == 'polygon':
            self._layer.add_polygons(data)
        elif shape_type == 'path':
            self._layer.add_paths(data)

setsAreEqual(a, b)

Convenience function. Return true if sets (a, b) are equal.

Source code in src/napari_layer_table/_my_layer.py
66
67
68
69
70
71
72
73
74
def setsAreEqual(a, b):
    """Convenience function. Return true if sets (a, b) are equal.
    """
    if len(a) != len(b):
        return False
    for x in a:
        if x not in b:
            return False
    return True