diff --git a/src/cmdforge/gui/pages/tool_builder_page.py b/src/cmdforge/gui/pages/tool_builder_page.py index 08535bd..da0c82f 100644 --- a/src/cmdforge/gui/pages/tool_builder_page.py +++ b/src/cmdforge/gui/pages/tool_builder_page.py @@ -241,6 +241,7 @@ class ToolBuilderPage(QWidget): self._flow_widget = FlowGraphWidget() self._flow_widget.node_double_clicked.connect(self._on_flow_node_double_clicked) self._flow_widget.steps_deleted.connect(self._on_flow_steps_deleted) + self._flow_widget.steps_reordered.connect(self._on_flow_steps_reordered) # Replace placeholder old_widget = self.steps_stack.widget(1) @@ -282,6 +283,35 @@ class ToolBuilderPage(QWidget): # Refresh the list view (flow view will be refreshed by set_tool) self._refresh_steps() + def _on_flow_steps_reordered(self, new_order: list): + """Handle step reordering from flow view. + + Args: + new_order: List of old indices in new order. + e.g., [0, 2, 1] means step 0 stays first, + step 2 moves to second, step 1 moves to third. + """ + if not self._tool or not self._tool.steps: + return + + # Reorder steps according to new_order + old_steps = self._tool.steps[:] + new_steps = [old_steps[i] for i in new_order if i < len(old_steps)] + + # Validate the new order + warnings = self._validate_step_order(new_steps) + + if warnings: + # Show warning but allow the reorder + warning_msg = "Variable dependency warnings:\n\n" + "\n".join(warnings) + warning_msg += "\n\nThe reorder has been applied. You may need to fix these issues." + QMessageBox.warning(self, "Dependency Warning", warning_msg) + + self._tool.steps = new_steps + + # Refresh both views + self._refresh_steps() + def _load_tool(self, name: str): """Load an existing tool for editing.""" tool = load_tool(name) diff --git a/src/cmdforge/gui/widgets/flow_graph.py b/src/cmdforge/gui/widgets/flow_graph.py index de2bf1d..bd733df 100644 --- a/src/cmdforge/gui/widgets/flow_graph.py +++ b/src/cmdforge/gui/widgets/flow_graph.py @@ -125,6 +125,9 @@ class FlowGraphWidget(QWidget): # Emitted when steps are deleted from flow view (list of step indices) steps_deleted = Signal(list) + # Emitted when steps are reordered (new order as list of step indices) + steps_reordered = Signal(list) + def __init__(self, parent=None): super().__init__(parent) self._tool: Optional[Tool] = None @@ -153,6 +156,7 @@ class FlowGraphWidget(QWidget): # Connect signals self._graph.node_double_clicked.connect(self._on_node_double_clicked) self._graph.nodes_deleted.connect(self._on_nodes_deleted) + self._graph.port_disconnected.connect(self._on_port_disconnected) # Add graph widget layout.addWidget(self._graph.widget, 1) @@ -427,6 +431,56 @@ class FlowGraphWidget(QWidget): self.steps_deleted.emit(deleted_indices) self.flow_changed.emit() + def _on_port_disconnected(self, input_port, output_port): + """Handle port disconnection - reorder steps. + + When a connection is broken: + 1. The node that lost its input moves to the end + 2. The chain reconnects (previous node → next node) + 3. Disconnected node attaches at the end before Output + """ + if not self._tool or not self._tool.steps: + return + + # Get the nodes involved + # input_port belongs to the node that lost its input + # output_port belongs to the node that was providing the input + disconnected_node = input_port.node() + source_node = output_port.node() + + # Only handle step nodes (not Input/Output) + if not hasattr(disconnected_node, '_step_index') or disconnected_node._step_index < 0: + return + + disconnected_idx = disconnected_node._step_index + + # Find the node that the disconnected node was connected to (its output target) + next_node = None + if disconnected_node.output_ports(): + out_port = disconnected_node.output_ports()[0] + connected_ports = out_port.connected_ports() + if connected_ports: + next_node = connected_ports[0].node() + + # Build new order: move disconnected step to end + old_order = list(range(len(self._tool.steps))) + old_order.remove(disconnected_idx) + old_order.append(disconnected_idx) + + # Reconnect the graph visually + # Connect source to next (if next is a step node) + if next_node and hasattr(next_node, '_step_index') and next_node._step_index >= 0: + if source_node.output_ports() and next_node.input_ports(): + source_node.output_ports()[0].connect_to(next_node.input_ports()[0]) + + # Connect the last step (before disconnected) to disconnected node + # and disconnected node to output + # This will be handled by the rebuild after reorder + + # Emit the new order + self.steps_reordered.emit(old_order) + self.flow_changed.emit() + def refresh(self): """Refresh the graph from current tool data.""" self._rebuild_graph()