Add step type icons and inline name editing
- Add icons module with programmatically drawn icons: - Speech bubble for prompt steps - Code brackets </> for code steps - Arrow icons for input/output nodes - Display icons in list view next to step names - Display icons in flow view node headers - Add inline name editing in flow view via property_changed signal - Sync name changes from flow view to tool model and list view Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -9,6 +9,10 @@ from PySide6.QtGui import QKeyEvent, QAction
|
||||
from NodeGraphQt import NodeGraph, BaseNode
|
||||
|
||||
from ...tool import Tool, PromptStep, CodeStep
|
||||
from .icons import (
|
||||
get_prompt_icon_path, get_code_icon_path,
|
||||
get_input_icon_path, get_output_icon_path
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -35,6 +39,7 @@ class InputNode(CmdForgeBaseNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_color(90, 90, 160) # Indigo
|
||||
self.set_icon(get_input_icon_path(16))
|
||||
# Default output for stdin
|
||||
self.add_output('input', color=(180, 180, 250))
|
||||
|
||||
@@ -56,7 +61,8 @@ class PromptNode(CmdForgeBaseNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_color(102, 126, 234) # Indigo (matching CmdForge theme)
|
||||
self.add_input('in', color=(180, 180, 250), multi_input=True)
|
||||
self.set_icon(get_prompt_icon_path(16))
|
||||
self.add_input('in', color=(180, 180, 250))
|
||||
self.add_output('out', color=(180, 250, 180))
|
||||
|
||||
# Add properties for display
|
||||
@@ -82,7 +88,8 @@ class CodeNode(CmdForgeBaseNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_color(72, 187, 120) # Green
|
||||
self.add_input('in', color=(180, 250, 180), multi_input=True)
|
||||
self.set_icon(get_code_icon_path(16))
|
||||
self.add_input('in', color=(180, 250, 180))
|
||||
self.add_output('out', color=(250, 220, 180))
|
||||
|
||||
# Add properties
|
||||
@@ -106,7 +113,8 @@ class OutputNode(CmdForgeBaseNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_color(237, 137, 54) # Orange
|
||||
self.add_input('in', color=(250, 220, 180), multi_input=True)
|
||||
self.set_icon(get_output_icon_path(16))
|
||||
self.add_input('in', color=(250, 220, 180))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -114,7 +122,12 @@ class OutputNode(CmdForgeBaseNode):
|
||||
# =============================================================================
|
||||
|
||||
class FlowGraphWidget(QWidget):
|
||||
"""Widget for visualizing tool flow as a node graph."""
|
||||
"""Widget for visualizing tool flow as a node graph.
|
||||
|
||||
Note: Currently the flow view is read-only for connections. Reordering
|
||||
steps by manipulating connections is a future enhancement (TODO).
|
||||
Use the list view for reordering steps via drag-drop.
|
||||
"""
|
||||
|
||||
# Emitted when a node is double-clicked (step_index, step_type)
|
||||
node_double_clicked = Signal(int, str)
|
||||
@@ -128,6 +141,9 @@ class FlowGraphWidget(QWidget):
|
||||
# Emitted when steps are reordered (new order as list of step indices)
|
||||
steps_reordered = Signal(list)
|
||||
|
||||
# Emitted when a step name is changed (step_index, new_name)
|
||||
step_name_changed = Signal(int, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tool: Optional[Tool] = None
|
||||
@@ -157,7 +173,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)
|
||||
self._graph.property_changed.connect(self._on_property_changed)
|
||||
|
||||
# Add graph widget
|
||||
layout.addWidget(self._graph.widget, 1)
|
||||
@@ -172,7 +188,6 @@ class FlowGraphWidget(QWidget):
|
||||
"Zoom: Scroll | "
|
||||
"Select: Click/drag | "
|
||||
"Edit: Double-click | "
|
||||
"Cut links: Alt+Shift+drag | "
|
||||
"Select All: A | "
|
||||
"Fit: F"
|
||||
)
|
||||
@@ -449,59 +464,23 @@ 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.
|
||||
def _on_property_changed(self, node, prop_name, value):
|
||||
"""Handle node property change.
|
||||
|
||||
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
|
||||
Filters for 'name' property to detect step name changes.
|
||||
"""
|
||||
# Ignore during rebuild
|
||||
if self._rebuilding:
|
||||
return
|
||||
|
||||
if not self._tool or not self._tool.steps:
|
||||
# Only interested in name changes
|
||||
if prop_name != 'name':
|
||||
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()
|
||||
if hasattr(node, '_step_index') and node._step_index >= 0:
|
||||
self.step_name_changed.emit(node._step_index, value)
|
||||
self.flow_changed.emit()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the graph from current tool data."""
|
||||
|
||||
Reference in New Issue
Block a user