diff --git a/src/napari_experimental/_widget.py b/src/napari_experimental/_widget.py index 5779a6f..0487886 100644 --- a/src/napari_experimental/_widget.py +++ b/src/napari_experimental/_widget.py @@ -20,6 +20,14 @@ class GroupLayerWidget(QWidget): + """ + Main plugin widget for interacting with GroupLayers. + + Parameters + ---------- + viewer : napari.viewer.Viewer + Main viewer instance containing (in particular) the LayerList. + """ @property def global_layers(self) -> LayerList: @@ -65,9 +73,12 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.layout().addWidget(self.group_layers_view) def _new_layer_group(self) -> None: - """ """ - # Still causes bugs when moving groups - # inside other groups, to investigate! + """ + Action taken when creating a new, empty layer group in the widget. + + TODO: Still causes a seg-fault when moving an empty group inside + another empty group. + """ self.group_layers.add_new_group() # BEGIN FUNCTIONS TO ENSURE CONSISTENCY BETWEEN @@ -112,8 +123,13 @@ def _new_layer_in_main_viewer(self, event: Event) -> None: def _on_layer_moved(self, event: Event) -> None: """ - Actions to take after GroupLayers are reordered: + Actions taken when Nodes with the GroupLayer object are reordered: - Impose layer order on the main viewer after an update. + + Parameters + ---------- + event : Event + Unused, but contains the old and new indices of the moved item. """ new_order = self.group_layers.flat_index_order() # Since the LayerList viewer indexes in the reverse to our Tree model, @@ -131,13 +147,28 @@ def _on_layer_moved(self, event: Event) -> None: def _removed_layer_in_main_viewer(self, event: Event) -> None: """ - :param event: index, value (layer that was removed) + Action taken when a layer is removed using the main LayerList + viewer. + + Parameters + ---------- + event : Event + Emitted event with attributes + - index (of removed layer in LayerList), + - value (the layer that was removed). """ self.group_layers.remove_layer_item(layer_ptr=event.value) def _removed_layer_in_group_layers(self, event: Event) -> None: """ - index, value + Action taken when a layer is removed using the GroupLayers view. + + Parameters + ---------- + event : Event + Emitted event with attributes + - index (of the layer that was removed), + - value (the layer that was removed). """ layer_to_remove = event.value.layer if layer_to_remove in self.global_layers: @@ -146,6 +177,11 @@ def _removed_layer_in_group_layers(self, event: Event) -> None: # END FUNCTION BLOCK def _enter_debug(self) -> None: - """Placeholder method that allows the developer to - enter a DEBUG context with the widget as self.""" + """ + Placeholder method that allows the developer to + enter a DEBUG context with the widget as self. + + Place a breakpoint at the pass statement below to + utilise. + """ pass diff --git a/src/napari_experimental/group_layer.py b/src/napari_experimental/group_layer.py index 869d6cf..116e3e9 100644 --- a/src/napari_experimental/group_layer.py +++ b/src/napari_experimental/group_layer.py @@ -22,13 +22,60 @@ def random_string(str_length: int = 5) -> str: class GroupLayer(Group[GroupLayerNode], GroupLayerNode): """ + A Group item for a tree-like data structure whose nodes have a dedicated + attribute for tracking a single Layer. See `napari.utils.tree` for more + information about Nodes and Groups. + GroupLayers are the "complex" component of the Tree structure that is used to organise Layers into Groups. A GroupLayer contains GroupLayerNodes and other GroupLayers (which are, in particular, a subclass of GroupLayerNode). + By convention, GroupLayers themselves do not track individual Layers, and + hence their `.layer` property is always set to `None`. Contrastingly, their + `.is_group()` method always returns `True` compared to a `GroupLayerNode`'s + method returning `False`. + + Since the Nodes in the tree map 1:1 with the Layers, the docstrings and + comments within this class often use the words interchangeably. This may + give rise to phrases such as "Layers in the model" even though - strictly + speaking - there are no Layers in the model, only GroupLayerNodes which + track the Layers. Such phrases should be taken to mean "Layers which are + tracked by one GroupLayerNode in the model", and typically serve to save on + the verbosity of comments. In places where this may give rise to ambiguity, + the precise language is used. + + Parameters + ---------- + *items_to_include : Layer | GroupLayerNode | GroupLayer + Items to be added (in the order they are given as arguments) to the + Group when it is instantiated. Layers will have a Node created to + track them, GroupLayerNodes will simply be added to the GroupLayer, + as will other GroupLayers. + + Attributes + ---------- + name + + Methods + ------- + add_new_layer + Add a new (GroupLayerNode tracking a) Layer to the GroupLayer. + add_new_group + Add a new GroupLayer inside this GroupLayer. + check_already_tracking + Check if a Layer is already tracked within the tree. + flat_index_order + Return a list of the tracked Layers, in the order of their occurrence + in the tree. + remove_layer_item + Remove any Nodes that track the Layer provided from the tree (if there + are any such Nodes). """ @property def name(self) -> str: + """ + Name of the GroupLayer. + """ return self._name @name.setter @@ -73,11 +120,13 @@ def _add_new_item( item_type: Literal["Node", "Group"], location: Optional[NestedIndex | int] = None, layer_ptr: Optional[Layer] = None, - group_items: Optional[Iterable[GroupLayer | GroupLayerNode]] = None, + group_items: Optional[ + Iterable[Layer | GroupLayer | GroupLayerNode] + ] = None, ) -> None: """ - Abstraction method handling the addition of Nodes and Groups to the - tree structure. + Abstract method handling the addition of Nodes and Groups to the + tree structure. See also `add_new_layer` and `add_new_group`. Parameters ---------- @@ -119,7 +168,7 @@ def _add_new_item( "A Layer must be provided when " "adding a Node to the GroupLayers tree." ) - elif insertion_group._check_already_tracking(layer_ptr=layer_ptr): + elif insertion_group.check_already_tracking(layer_ptr=layer_ptr): raise RuntimeError( f"Group {insertion_group} is already tracking {layer_ptr}" ) @@ -131,30 +180,6 @@ def _add_new_item( group_items = () insertion_group.insert(insertion_index, GroupLayer(*group_items)) - def _check_already_tracking( - self, layer_ptr: Layer, recursive: bool = True - ) -> bool: - """ - Return TRUE if the layer provided is already being tracked - by a Node in this tree. - - Layer equality is determined by the IS keyword, to confirm that - a Node is pointing to the Layer object in memory. - - :param layer_ptr: Reference to the Layer to determine is in the - model. - :param recursive: If True, then all sub-trees of the tree will be - checked for the given Layer, returning True if it is found at any - depth. - """ - for item in self: - if not item.is_group() and item.layer is layer_ptr: - return True - elif item.is_group() and recursive: - if item._check_already_tracking(layer_ptr, recursive=True): - return True - return False - def _node_name(self) -> str: """Will be used when rendering node tree as string.""" return f"GL-{self.name}" @@ -165,7 +190,7 @@ def add_new_layer( location: Optional[NestedIndex | int] = None, ) -> None: """ - Add a new (Node tracking a) layer to the model. + Add a new (Node tracking a) Layer to the model. New Nodes are by default added at the bottom of the tree. Parameters @@ -179,7 +204,7 @@ def add_new_layer( def add_new_group( self, - *items: GroupLayer | GroupLayerNode, + *items: Layer | GroupLayer | GroupLayerNode, location: Optional[NestedIndex | int] = None, ) -> None: """ @@ -190,19 +215,48 @@ def add_new_group( ---------- location: NestedIndex | int, optional Location at which to insert the new GroupLayer. - items: GroupLayer | GroupLayerNode, optional + items: Layer | GroupLayer | GroupLayerNode, optional Items to add to the new GroupLayer upon its creation. """ self._add_new_item("Group", location=location, group_items=items) + def check_already_tracking( + self, layer_ptr: Layer, recursive: bool = True + ) -> bool: + """ + Return TRUE if the Layer provided is already being tracked + by a Node in this tree. + + Layer equality is determined by the IS keyword, to confirm that + a Node is pointing to the Layer object in memory. + + Parameters + ---------- + layer_ptr : Layer + The Layer to determine is in the tree (or not). + recursive: bool, default = True + If True, then all sub-trees of the tree will be checked for the + given Layer, returning True if it is found at any depth. + """ + for item in self: + if not item.is_group() and item.layer is layer_ptr: + return True + elif item.is_group() and recursive: + item: GroupLayer + if item.check_already_tracking(layer_ptr, recursive=True): + return True + return False + def flat_index_order(self) -> List[NestedIndex]: """ Return a list of NestedIndex-es, whose order corresponds to the flat order of the Nodes in the tree. The flat order of the Nodes counts up from 0 at the root of the - tree, and descends into branches before continuing. An example is - given in the tree below: + tree, and descends down into the tree, exhausting branches it + encounters before continuing. + + An example is given in the tree below: Tree Flat Index - Node_0 0 @@ -214,7 +268,6 @@ def flat_index_order(self) -> List[NestedIndex]: - Node_AA0 5 - Node_A2 6 - Node_2 7 - ... """ order: List[NestedIndex] = [] for item in self: @@ -231,21 +284,12 @@ def flat_index_order(self) -> List[NestedIndex]: def is_group(self) -> bool: """ - Determines if this item is a genuine Node, or a branch - containing further nodes. + Determines if this item is a genuine Node, or a branch containing + further nodes. This method is explicitly defined to ensure that we can distinguish between genuine GroupLayerNodes and GroupLayers when traversing the tree. - - Due to (necessarily) being a subclass of GroupLayerNode, it is possible - for GroupLayer instances to have the .layer property set to track a - Layer object. This is (currently) not intended behaviour - GroupLayers - are not meant to track Layers themselves. However it is possible to - manually set the layer tracker, and I can foresee a situation in the - future where this is desirable (particularly for rendering or drawing - purposes), so am not strictly forbidding this by overwriting the .layer - setter. """ return True # A GroupLayer is ALWAYS a branch. @@ -255,8 +299,8 @@ def remove_layer_item(self, layer_ptr: Layer, prune: bool = True) -> None: Layer from the tree model. If removing a layer would result in one of the Group being empty, - then the empty Group is also removed from the model. - This can be toggled with the `prune` argument. + then the empty Group is also removed from the model. This can be + toggled with the `prune` argument. Note that the `is` keyword is used to determine equality between the layer provided and the layers that are tracked by the Nodes. @@ -264,10 +308,13 @@ def remove_layer_item(self, layer_ptr: Layer, prune: bool = True) -> None: tracking from the model, rather than removing the Layer from memory itself (as there may still be hanging references to it). - :param layer_ptr: All Nodes tracking layer_ptr will be removed from - the model. - :param prune: If True, branches that are empty after removing the - layer in question will also be removed. + Parameters + ---------- + layer_ptr : Layer + All Nodes tracking this Layer will be removed from the model. + prune : bool, default = True + If True, branches that are empty after removing the Layer in + question will also be removed. """ for node in self: if node.is_group(): diff --git a/src/napari_experimental/group_layer_node.py b/src/napari_experimental/group_layer_node.py index fd42cf1..8064fac 100644 --- a/src/napari_experimental/group_layer_node.py +++ b/src/napari_experimental/group_layer_node.py @@ -7,6 +7,30 @@ class GroupLayerNode(Node): + """ + A Node item for a tree-like data structure that has a dedicated attribute + for tracking a single Layer. See `napari.utils.tree` for more information + about Nodes. + + GroupLayerNodes are the core building block of the GroupLayer display. By + wrapping a Layer inside a Node in this way, Layers can be organised into a + tree-like structure (allowing for grouping and nesting) without the hassle + of subclassing or mixing-in the Node class (which would require widespread + changes to the core napari codebase). + + Parameters + ---------- + layer_ptr : Layer, optional + The Layer object that this Node should initially track. + name: str, optional + Name to be given to the Node upon creation. The Layer retains its name. + + Attributes + ---------- + is_tracking + layer + name + """ __default_name: str = "Node[None]" @@ -14,10 +38,17 @@ class GroupLayerNode(Node): @property def is_tracking(self) -> bool: + """ + Returns True if the Node is currently tracking a Layer + (self.Layer is not None), else False. + """ return self.layer is not None @property def layer(self) -> Layer: + """ + The (pointer to the) Layer that the Node is currently tracking. + """ return self._tracking_layer @layer.setter @@ -29,6 +60,12 @@ def layer(self, new_ptr: Layer) -> None: @property def name(self) -> str: + """ + Name of the Node. + + If the Node is tracking a Layer, returns the name of the Layer. + Otherwise, returns the internal name of the Node. + """ if self.is_tracking: return self.layer.name else: diff --git a/src/napari_experimental/group_layer_qt.py b/src/napari_experimental/group_layer_qt.py index 082c282..f8d252b 100644 --- a/src/napari_experimental/group_layer_qt.py +++ b/src/napari_experimental/group_layer_qt.py @@ -12,6 +12,17 @@ class QtGroupLayerModel(QtNodeTreeModel[GroupLayer]): + """ + A QTreeModel that works with the GroupLayer tree structure. + See `napari._qt.containers.QtNodeTreeModel` for more information. + + Parameters + ---------- + root : GroupLayer + The root object from which to form the model. + parent : QWidget, optional + Parent QObject for the instance. + """ def __init__(self, root: GroupLayer, parent: QWidget = None): super().__init__(root, parent) @@ -48,6 +59,18 @@ def setData( class QtGroupLayerView(QtNodeTreeView): + """ + A QTreeView that works with the QtGroupLayerModel model. + See `napari._qt.containers.QtNodeTreeView` for more information. + + Parameters + ---------- + root : GroupLayer + The root object from which to form the model. + parent : QWidget, optional + Parent QObject for the instance. + """ + _root: GroupLayer model_class = QtGroupLayerModel diff --git a/tests/test_group_layer.py b/tests/test_group_layer.py index a42de98..49e319c 100644 --- a/tests/test_group_layer.py +++ b/tests/test_group_layer.py @@ -80,13 +80,13 @@ def test_check_is_already_tracking( expected_result: bool, ): assert ( - nested_layer_group._check_already_tracking( + nested_layer_group.check_already_tracking( layer_ptr=collection_of_layers[layer_key], recursive=recursive ) == expected_result ), ( f"Incorrect result (expected {expected_result}) " - f"for _check_already_tracking (with recursive = {recursive})" + f"for check_already_tracking (with recursive = {recursive})" ) @@ -184,9 +184,9 @@ def test_add_group( ].is_group(), "Group added in the incorrect location." added_group: GroupLayer = nested_layer_group[add_at_location] - assert added_group._check_already_tracking( + assert added_group.check_already_tracking( pts_1 - ) and added_group._check_already_tracking( + ) and added_group.check_already_tracking( pts_2 ), "Points layers were not added to the new Group upon creation." assert (