diff --git a/docs/guides/gui.md b/docs/guides/gui.md index 813ed68fa..0cda29288 100644 --- a/docs/guides/gui.md +++ b/docs/guides/gui.md @@ -148,6 +148,8 @@ Note that many of the menu command have keyboard shortcuts which can be configur **Click** elsewhere on image: Clear selection +**Control + left-click (hold) + drag** on instance: Duplicate the selected instance and drag the entire copied instance + ## Navigation Keys **Right arrow**: Move one frame forward diff --git a/sleap/gui/widgets/video.py b/sleap/gui/widgets/video.py index 08ee5bf36..593a8889e 100644 --- a/sleap/gui/widgets/video.py +++ b/sleap/gui/widgets/video.py @@ -12,6 +12,7 @@ >>> vp.addInstance(instance=my_instance, color=(r, g, b)) """ + from collections import deque # FORCE_REQUESTS controls whether we emit a signal to process frame requests @@ -1568,6 +1569,9 @@ def mousePressEvent(self, event): # Shift-click to mark all points as complete elif event.modifiers() == Qt.ShiftModifier: self.parentObject().updatePoints(complete=True, user_change=True) + # Ctrl-click to duplicate instance + elif event.modifiers() == Qt.ControlModifier: + self.parentObject().mousePressEvent(event) else: self.dragParent = False super(QtNode, self).mousePressEvent(event) @@ -1886,7 +1890,7 @@ def __init__( self.track_label.setHtml(instance_label_text) # Add nodes - for (node, point) in self.instance.nodes_points: + for node, point in self.instance.nodes_points: if point.visible or self.show_non_visible: node_item = QtNode( parent=self, @@ -1901,7 +1905,7 @@ def __init__( self.nodes[node.name] = node_item # Add edges - for (src, dst) in self.skeleton.edge_names: + for src, dst in self.skeleton.edge_names: # Make sure that both nodes are present in this instance before drawing edge if src in self.nodes and dst in self.nodes: edge_item = QtEdge( @@ -2124,6 +2128,75 @@ def hoverLeaveEvent(self, event): self.updateBox() return super().hoverLeaveEvent(event) + def mousePressEvent(self, event): + """Custom event handler for mouse press.""" + if event.buttons() == Qt.LeftButton: + if event.modifiers() == Qt.ControlModifier: + self.duplicate_instance() + else: + # Default behavior is to select the instance + super(QtInstance, self).mousePressEvent(event) + + def duplicate_instance(self): + """Duplicate the instance and add it to the scene.""" + # Add instance to the context + if self.player.context is None: + return + + # Copy the instance and add it to the context + context = self.player.context + context.newInstance(copy_instance=self.instance) + + # Find the new instance and its last label + lf = context.labels.find( + context.state["video"], context.state["frame_idx"], return_new=True + )[0] + new_instance = lf.instances[-1] + + # Select the duplicated QtInstance object + self.player.state["instance"] = new_instance + + # Refresh the plot + self.player.plot() + + def on_selection_update(): + """Callback to set the new QtInstance to be movable.""" + # Find the QtInstance corresponding to the newly created instance + for qt_inst in self.player.view.all_instances: + if qt_inst.instance == new_instance: + self.player.view.updatedSelection.disconnect(on_selection_update) + + # Set this QtInstance to be movable + qt_inst.setFlag(QGraphicsItem.ItemIsMovable) + + # Optionally grab the mouse and change cursor, so user can immediately drag + qt_inst.setCursor(Qt.ClosedHandCursor) + qt_inst.grabMouse() + break + + # Connect the callback to the updatedSelection signal + self.player.view.updatedSelection.connect(on_selection_update) + self.player.view.updatedSelection.emit() + + def mouseMoveEvent(self, event): + """Custom event handler to emit signal on event.""" + is_move = self.flags() & QGraphicsItem.ItemIsMovable + is_ctrl_pressed = (event.modifiers() & Qt.ControlModifier) == Qt.ControlModifier + is_alt_pressed = (event.modifiers() & Qt.AltModifier) == Qt.AltModifier + + # Only allow moving if the instance is selected + if is_move and (is_ctrl_pressed or is_alt_pressed): + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """Custom event handler for mouse release.""" + if self.flags() & QGraphicsItem.ItemIsMovable: + self.setFlag(QGraphicsItem.ItemIsMovable, False) + self.updatePoints(user_change=True) + self.updateBox() + self.ungrabMouse() + super().mouseReleaseEvent(event) + class VisibleBoundingBox(QtWidgets.QGraphicsRectItem): """QGraphicsRectItem for user instance bounding boxes.