"""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)