- Add ToolStepDialog for configuring tool steps (tool selection, input mapping, args) - Add "Add Tool" button in Tool Builder alongside Add Prompt/Add Code - Add ToolNode in flow graph visualization (purple puzzle piece icon) - Auto-populate dependencies when adding ToolStep in GUI - Add --auto-install flag for automatic runtime dependency installation - Add check_dependencies() and auto_install_dependencies() functions - Update runner to pass auto_install parameter through execution chain Co-Authored-By: Claude Opus 4.5 <[email protected]>
336 lines
9.0 KiB
Python
336 lines
9.0 KiB
Python
"""Step type icons for CmdForge GUI."""
|
|
|
|
from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QPen, QBrush, QPainterPath, QFont
|
|
from PySide6.QtCore import Qt, QRect, QRectF
|
|
|
|
|
|
def create_prompt_icon(size: int = 24, color: QColor = None) -> QIcon:
|
|
"""Create a speech bubble icon for prompt steps.
|
|
|
|
Args:
|
|
size: Icon size in pixels
|
|
color: Icon color (defaults to indigo #667eea)
|
|
"""
|
|
if color is None:
|
|
color = QColor(102, 126, 234) # Indigo
|
|
|
|
pixmap = QPixmap(size, size)
|
|
pixmap.fill(Qt.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
# Draw speech bubble
|
|
pen = QPen(color, 1.5)
|
|
painter.setPen(pen)
|
|
painter.setBrush(QBrush(color))
|
|
|
|
# Bubble body
|
|
margin = size * 0.1
|
|
bubble_rect = QRectF(margin, margin, size - 2 * margin, size * 0.65)
|
|
painter.drawRoundedRect(bubble_rect, 4, 4)
|
|
|
|
# Bubble tail (triangle pointing down-left)
|
|
path = QPainterPath()
|
|
tail_x = margin + size * 0.2
|
|
tail_y = bubble_rect.bottom()
|
|
path.moveTo(tail_x, tail_y - 2)
|
|
path.lineTo(tail_x - size * 0.1, tail_y + size * 0.2)
|
|
path.lineTo(tail_x + size * 0.15, tail_y - 2)
|
|
path.closeSubpath()
|
|
painter.drawPath(path)
|
|
|
|
painter.end()
|
|
return QIcon(pixmap)
|
|
|
|
|
|
def create_code_icon(size: int = 24, color: QColor = None) -> QIcon:
|
|
"""Create a code brackets icon for code steps.
|
|
|
|
Args:
|
|
size: Icon size in pixels
|
|
color: Icon color (defaults to green #48bb78)
|
|
"""
|
|
if color is None:
|
|
color = QColor(72, 187, 120) # Green
|
|
|
|
pixmap = QPixmap(size, size)
|
|
pixmap.fill(Qt.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
pen = QPen(color, 2.0)
|
|
pen.setCapStyle(Qt.RoundCap)
|
|
painter.setPen(pen)
|
|
|
|
# Draw < > brackets
|
|
margin = size * 0.15
|
|
mid_y = size / 2
|
|
bracket_height = size * 0.5
|
|
|
|
# Left bracket <
|
|
left_x = margin + size * 0.1
|
|
painter.drawLine(
|
|
int(left_x + size * 0.2), int(mid_y - bracket_height / 2),
|
|
int(left_x), int(mid_y)
|
|
)
|
|
painter.drawLine(
|
|
int(left_x), int(mid_y),
|
|
int(left_x + size * 0.2), int(mid_y + bracket_height / 2)
|
|
)
|
|
|
|
# Right bracket >
|
|
right_x = size - margin - size * 0.1
|
|
painter.drawLine(
|
|
int(right_x - size * 0.2), int(mid_y - bracket_height / 2),
|
|
int(right_x), int(mid_y)
|
|
)
|
|
painter.drawLine(
|
|
int(right_x), int(mid_y),
|
|
int(right_x - size * 0.2), int(mid_y + bracket_height / 2)
|
|
)
|
|
|
|
# Slash in middle /
|
|
slash_width = size * 0.12
|
|
painter.drawLine(
|
|
int(size / 2 + slash_width), int(mid_y - bracket_height / 2.5),
|
|
int(size / 2 - slash_width), int(mid_y + bracket_height / 2.5)
|
|
)
|
|
|
|
painter.end()
|
|
return QIcon(pixmap)
|
|
|
|
|
|
def create_input_icon(size: int = 24, color: QColor = None) -> QIcon:
|
|
"""Create an input icon (arrow pointing right into box).
|
|
|
|
Args:
|
|
size: Icon size in pixels
|
|
color: Icon color (defaults to indigo #5a5a9f)
|
|
"""
|
|
if color is None:
|
|
color = QColor(90, 90, 160) # Indigo
|
|
|
|
pixmap = QPixmap(size, size)
|
|
pixmap.fill(Qt.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
pen = QPen(color, 2.0)
|
|
pen.setCapStyle(Qt.RoundCap)
|
|
painter.setPen(pen)
|
|
|
|
margin = size * 0.15
|
|
mid_y = size / 2
|
|
|
|
# Arrow pointing right
|
|
arrow_start = margin
|
|
arrow_end = size - margin - size * 0.15
|
|
|
|
# Arrow shaft
|
|
painter.drawLine(int(arrow_start), int(mid_y), int(arrow_end), int(mid_y))
|
|
|
|
# Arrow head
|
|
head_size = size * 0.2
|
|
painter.drawLine(
|
|
int(arrow_end), int(mid_y),
|
|
int(arrow_end - head_size), int(mid_y - head_size)
|
|
)
|
|
painter.drawLine(
|
|
int(arrow_end), int(mid_y),
|
|
int(arrow_end - head_size), int(mid_y + head_size)
|
|
)
|
|
|
|
painter.end()
|
|
return QIcon(pixmap)
|
|
|
|
|
|
def create_output_icon(size: int = 24, color: QColor = None) -> QIcon:
|
|
"""Create an output icon (arrow pointing right out of box).
|
|
|
|
Args:
|
|
size: Icon size in pixels
|
|
color: Icon color (defaults to orange #ed8936)
|
|
"""
|
|
if color is None:
|
|
color = QColor(237, 137, 54) # Orange
|
|
|
|
pixmap = QPixmap(size, size)
|
|
pixmap.fill(Qt.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
pen = QPen(color, 2.0)
|
|
pen.setCapStyle(Qt.RoundCap)
|
|
painter.setPen(pen)
|
|
|
|
margin = size * 0.15
|
|
mid_y = size / 2
|
|
|
|
# Arrow pointing right (export style)
|
|
arrow_start = margin + size * 0.15
|
|
arrow_end = size - margin
|
|
|
|
# Arrow shaft
|
|
painter.drawLine(int(arrow_start), int(mid_y), int(arrow_end), int(mid_y))
|
|
|
|
# Arrow head
|
|
head_size = size * 0.2
|
|
painter.drawLine(
|
|
int(arrow_end), int(mid_y),
|
|
int(arrow_end - head_size), int(mid_y - head_size)
|
|
)
|
|
painter.drawLine(
|
|
int(arrow_end), int(mid_y),
|
|
int(arrow_end - head_size), int(mid_y + head_size)
|
|
)
|
|
|
|
painter.end()
|
|
return QIcon(pixmap)
|
|
|
|
|
|
# Cached icons for reuse
|
|
_icon_cache = {}
|
|
|
|
# Cached icon file paths for NodeGraphQt (which requires file paths)
|
|
_icon_path_cache = {}
|
|
|
|
|
|
def _get_icon_path(icon_type: str, size: int, create_func) -> str:
|
|
"""Get or create a cached icon file path.
|
|
|
|
NodeGraphQt requires file paths for icons, so we save to temp files.
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
key = (icon_type, size)
|
|
if key not in _icon_path_cache:
|
|
icon = create_func(size)
|
|
pixmap = icon.pixmap(size, size)
|
|
|
|
# Save to temp directory with a stable name
|
|
temp_dir = os.path.join(tempfile.gettempdir(), 'cmdforge_icons')
|
|
os.makedirs(temp_dir, exist_ok=True)
|
|
|
|
path = os.path.join(temp_dir, f'{icon_type}_{size}.png')
|
|
pixmap.save(path, 'PNG')
|
|
_icon_path_cache[key] = path
|
|
|
|
return _icon_path_cache[key]
|
|
|
|
|
|
def get_prompt_icon_path(size: int = 16) -> str:
|
|
"""Get file path to prompt icon (for NodeGraphQt)."""
|
|
return _get_icon_path('prompt', size, create_prompt_icon)
|
|
|
|
|
|
def get_code_icon_path(size: int = 16) -> str:
|
|
"""Get file path to code icon (for NodeGraphQt)."""
|
|
return _get_icon_path('code', size, create_code_icon)
|
|
|
|
|
|
def get_input_icon_path(size: int = 16) -> str:
|
|
"""Get file path to input icon (for NodeGraphQt)."""
|
|
return _get_icon_path('input', size, create_input_icon)
|
|
|
|
|
|
def get_output_icon_path(size: int = 16) -> str:
|
|
"""Get file path to output icon (for NodeGraphQt)."""
|
|
return _get_icon_path('output', size, create_output_icon)
|
|
|
|
|
|
def get_prompt_icon(size: int = 24) -> QIcon:
|
|
"""Get cached prompt icon."""
|
|
key = ('prompt', size)
|
|
if key not in _icon_cache:
|
|
_icon_cache[key] = create_prompt_icon(size)
|
|
return _icon_cache[key]
|
|
|
|
|
|
def get_code_icon(size: int = 24) -> QIcon:
|
|
"""Get cached code icon."""
|
|
key = ('code', size)
|
|
if key not in _icon_cache:
|
|
_icon_cache[key] = create_code_icon(size)
|
|
return _icon_cache[key]
|
|
|
|
|
|
def get_input_icon(size: int = 24) -> QIcon:
|
|
"""Get cached input icon."""
|
|
key = ('input', size)
|
|
if key not in _icon_cache:
|
|
_icon_cache[key] = create_input_icon(size)
|
|
return _icon_cache[key]
|
|
|
|
|
|
def get_output_icon(size: int = 24) -> QIcon:
|
|
"""Get cached output icon."""
|
|
key = ('output', size)
|
|
if key not in _icon_cache:
|
|
_icon_cache[key] = create_output_icon(size)
|
|
return _icon_cache[key]
|
|
|
|
|
|
def create_tool_icon(size: int = 24, color: QColor = None) -> QIcon:
|
|
"""Create a puzzle piece icon for tool steps (calling another tool).
|
|
|
|
Args:
|
|
size: Icon size in pixels
|
|
color: Icon color (defaults to purple #9f7aea)
|
|
"""
|
|
if color is None:
|
|
color = QColor(159, 122, 234) # Purple
|
|
|
|
pixmap = QPixmap(size, size)
|
|
pixmap.fill(Qt.transparent)
|
|
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
pen = QPen(color, 1.5)
|
|
painter.setPen(pen)
|
|
painter.setBrush(QBrush(color))
|
|
|
|
# Draw a simple puzzle piece shape
|
|
margin = size * 0.12
|
|
w = size - 2 * margin
|
|
h = size - 2 * margin
|
|
|
|
path = QPainterPath()
|
|
# Start from top-left, going clockwise
|
|
path.moveTo(margin, margin + h * 0.3)
|
|
# Top edge with bump
|
|
path.lineTo(margin + w * 0.35, margin + h * 0.3)
|
|
path.arcTo(QRectF(margin + w * 0.35 - w * 0.1, margin, w * 0.3, h * 0.3), 180, -180)
|
|
path.lineTo(margin + w, margin + h * 0.3)
|
|
# Right edge
|
|
path.lineTo(margin + w, margin + h * 0.7)
|
|
# Bottom edge with notch
|
|
path.lineTo(margin + w * 0.65, margin + h * 0.7)
|
|
path.arcTo(QRectF(margin + w * 0.35, margin + h * 0.7, w * 0.3, h * 0.3), 0, 180)
|
|
path.lineTo(margin, margin + h * 0.7)
|
|
# Left edge back to start
|
|
path.closeSubpath()
|
|
|
|
painter.drawPath(path)
|
|
painter.end()
|
|
|
|
return QIcon(pixmap)
|
|
|
|
|
|
def get_tool_icon(size: int = 24) -> QIcon:
|
|
"""Get cached tool icon."""
|
|
key = ('tool', size)
|
|
if key not in _icon_cache:
|
|
_icon_cache[key] = create_tool_icon(size)
|
|
return _icon_cache[key]
|
|
|
|
|
|
def get_tool_icon_path(size: int = 16) -> str:
|
|
"""Get file path to tool icon (for NodeGraphQt)."""
|
|
return _get_icon_path('tool', size, create_tool_icon)
|