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