IncludeCPP 3.3.11__py3-none-any.whl → 3.3.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2720 @@
1
+ """
2
+ IncludeCPP Project Interface with CodeMaker
3
+ Professional visual system design and planning tool
4
+
5
+ Features:
6
+ - Modern dark frameless window with custom controls
7
+ - Interactive node-based mindmap canvas
8
+ - 24+ node types with categorized menus
9
+ - Pan/zoom navigation with smooth animations
10
+ - Multi-selection with rubber band
11
+ - Undo/redo system
12
+ - Copy/paste/duplicate
13
+ - Node grouping
14
+ - Code generation (C++/Python)
15
+ - Template system
16
+ - Search and filter
17
+ - Minimap overview
18
+ - Keyboard shortcuts
19
+ - Export (PNG/SVG/Code)
20
+ - Cross-platform support
21
+ """
22
+
23
+ import sys
24
+ import json
25
+ import math
26
+ import uuid
27
+ import copy
28
+ from pathlib import Path
29
+ from typing import Optional, List, Dict, Tuple, Any, Set, Union
30
+ from dataclasses import dataclass, field, asdict
31
+ from datetime import datetime
32
+ from enum import Enum
33
+
34
+ try:
35
+ from PyQt6.QtWidgets import (
36
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
37
+ QLabel, QPushButton, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem,
38
+ QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem,
39
+ QGraphicsTextItem, QGraphicsLineItem, QGraphicsEllipseItem,
40
+ QMenu, QInputDialog, QMessageBox, QGraphicsDropShadowEffect,
41
+ QScrollArea, QSizePolicy, QGraphicsPathItem, QToolBar, QStatusBar,
42
+ QGraphicsProxyWidget, QLineEdit, QTextEdit, QDialog, QDialogButtonBox,
43
+ QFormLayout, QComboBox, QSpinBox, QColorDialog, QGraphicsPolygonItem,
44
+ QToolButton, QWidgetAction, QSlider, QProgressBar, QFileDialog,
45
+ QTabWidget, QListWidget, QListWidgetItem, QCheckBox, QGridLayout,
46
+ QGroupBox, QPlainTextEdit, QRubberBand, QStackedWidget, QDockWidget
47
+ )
48
+ from PyQt6.QtCore import (
49
+ Qt, QRectF, QPointF, QTimer, QPropertyAnimation, QEasingCurve,
50
+ pyqtSignal, QObject, QLineF, QSize, QParallelAnimationGroup,
51
+ QSequentialAnimationGroup, pyqtProperty, QVariantAnimation,
52
+ QMimeData, QByteArray, QBuffer, QIODevice, QRect
53
+ )
54
+ from PyQt6.QtGui import (
55
+ QFont, QColor, QPen, QBrush, QPainter, QPainterPath,
56
+ QLinearGradient, QRadialGradient, QCursor, QWheelEvent,
57
+ QMouseEvent, QKeyEvent, QTransform, QPolygonF, QFontMetrics,
58
+ QPalette, QPixmap, QIcon, QAction, QClipboard, QImage,
59
+ QKeySequence, QFontDatabase, QGuiApplication, QUndoStack,
60
+ QUndoCommand, QShortcut
61
+ )
62
+ PYQT_AVAILABLE = True
63
+ except ImportError:
64
+ PYQT_AVAILABLE = False
65
+
66
+
67
+ # ============================================================================
68
+ # Cross-Platform Support
69
+ # ============================================================================
70
+
71
+ def get_system_font() -> str:
72
+ """Get platform-appropriate system font."""
73
+ if sys.platform == "win32":
74
+ return "Segoe UI"
75
+ elif sys.platform == "darwin":
76
+ return "SF Pro"
77
+ else:
78
+ return "Ubuntu"
79
+
80
+ def get_monospace_font() -> str:
81
+ """Get platform-appropriate monospace font."""
82
+ if sys.platform == "win32":
83
+ return "Consolas"
84
+ elif sys.platform == "darwin":
85
+ return "SF Mono"
86
+ else:
87
+ return "Ubuntu Mono"
88
+
89
+ def get_dpi_scale() -> float:
90
+ """Get DPI scale factor for high-DPI displays."""
91
+ if PYQT_AVAILABLE:
92
+ app = QApplication.instance()
93
+ if app:
94
+ screen = app.primaryScreen()
95
+ if screen:
96
+ return screen.logicalDotsPerInch() / 96.0
97
+ return 1.0
98
+
99
+
100
+ # ============================================================================
101
+ # Constants & Theme
102
+ # ============================================================================
103
+
104
+ SYSTEM_FONT = get_system_font() if PYQT_AVAILABLE else "Segoe UI"
105
+ MONO_FONT = get_monospace_font() if PYQT_AVAILABLE else "Consolas"
106
+
107
+ THEME = {
108
+ 'bg_dark': '#0d0d0d',
109
+ 'bg_primary': '#1a1a1a',
110
+ 'bg_secondary': '#252525',
111
+ 'bg_tertiary': '#2d2d2d',
112
+ 'bg_hover': '#3d3d3d',
113
+ 'border': '#3d3d3d',
114
+ 'border_light': '#4d4d4d',
115
+ 'text_primary': '#e0e0e0',
116
+ 'text_secondary': '#a0a0a0',
117
+ 'text_muted': '#666666',
118
+ 'accent_blue': '#4a9eff',
119
+ 'accent_green': '#50c878',
120
+ 'accent_orange': '#ff9800',
121
+ 'accent_purple': '#e040fb',
122
+ 'accent_red': '#ff5555',
123
+ 'accent_cyan': '#00bcd4',
124
+ 'accent_yellow': '#ffeb3b',
125
+ 'accent_pink': '#ff4081',
126
+ 'accent_teal': '#009688',
127
+ 'accent_indigo': '#3f51b5',
128
+ 'accent_lime': '#cddc39',
129
+ 'success': '#4caf50',
130
+ 'warning': '#ff9800',
131
+ 'error': '#f44336',
132
+ 'shadow': 'rgba(0, 0, 0, 0.4)',
133
+ }
134
+
135
+ # Expanded node types with categories
136
+ NODE_CATEGORIES = {
137
+ 'Code Structures': ['class', 'struct', 'enum', 'interface', 'template_class', 'union'],
138
+ 'Functions': ['function', 'method', 'constructor', 'destructor', 'lambda', 'operator'],
139
+ 'Data': ['object', 'variable', 'constant', 'definition', 'typedef', 'pointer'],
140
+ 'Organization': ['namespace', 'module', 'package', 'file', 'folder', 'header'],
141
+ 'Flow': ['condition', 'loop', 'exception', 'async', 'callback', 'event'],
142
+ }
143
+
144
+ NODE_TYPES = {
145
+ # Code Structures
146
+ 'class': {'color': '#4a9eff', 'icon': 'C', 'gradient_start': '#5aaeff', 'gradient_end': '#3a8eef', 'label': 'Class', 'category': 'Code Structures'},
147
+ 'struct': {'color': '#9c27b0', 'icon': 'S', 'gradient_start': '#ac37c0', 'gradient_end': '#8c17a0', 'label': 'Struct', 'category': 'Code Structures'},
148
+ 'enum': {'color': '#ffeb3b', 'icon': 'E', 'gradient_start': '#fff34b', 'gradient_end': '#e0cc2b', 'label': 'Enum', 'category': 'Code Structures'},
149
+ 'interface': {'color': '#00bcd4', 'icon': 'I', 'gradient_start': '#20cce4', 'gradient_end': '#00acc4', 'label': 'Interface', 'category': 'Code Structures'},
150
+ 'template_class': {'color': '#7c4dff', 'icon': 'TC', 'gradient_start': '#8c5dff', 'gradient_end': '#6c3def', 'label': 'Template Class', 'category': 'Code Structures'},
151
+ 'union': {'color': '#795548', 'icon': 'U', 'gradient_start': '#896558', 'gradient_end': '#694538', 'label': 'Union', 'category': 'Code Structures'},
152
+
153
+ # Functions
154
+ 'function': {'color': '#50c878', 'icon': 'fn', 'gradient_start': '#60d888', 'gradient_end': '#40b868', 'label': 'Function', 'category': 'Functions'},
155
+ 'method': {'color': '#66bb6a', 'icon': 'm', 'gradient_start': '#76cb7a', 'gradient_end': '#56ab5a', 'label': 'Method', 'category': 'Functions'},
156
+ 'constructor': {'color': '#81c784', 'icon': 'ctor', 'gradient_start': '#91d794', 'gradient_end': '#71b774', 'label': 'Constructor', 'category': 'Functions'},
157
+ 'destructor': {'color': '#a5d6a7', 'icon': 'dtor', 'gradient_start': '#b5e6b7', 'gradient_end': '#95c697', 'label': 'Destructor', 'category': 'Functions'},
158
+ 'lambda': {'color': '#4db6ac', 'icon': 'λ', 'gradient_start': '#5dc6bc', 'gradient_end': '#3da69c', 'label': 'Lambda', 'category': 'Functions'},
159
+ 'operator': {'color': '#26a69a', 'icon': 'op', 'gradient_start': '#36b6aa', 'gradient_end': '#16968a', 'label': 'Operator', 'category': 'Functions'},
160
+
161
+ # Data
162
+ 'object': {'color': '#e040fb', 'icon': 'O', 'gradient_start': '#f050ff', 'gradient_end': '#c030db', 'label': 'Object', 'category': 'Data'},
163
+ 'variable': {'color': '#ba68c8', 'icon': 'var', 'gradient_start': '#ca78d8', 'gradient_end': '#aa58b8', 'label': 'Variable', 'category': 'Data'},
164
+ 'constant': {'color': '#ce93d8', 'icon': 'const', 'gradient_start': '#dea3e8', 'gradient_end': '#be83c8', 'label': 'Constant', 'category': 'Data'},
165
+ 'definition': {'color': '#ff9800', 'icon': 'D', 'gradient_start': '#ffa820', 'gradient_end': '#e08800', 'label': 'Definition', 'category': 'Data'},
166
+ 'typedef': {'color': '#ffb74d', 'icon': 'T', 'gradient_start': '#ffc75d', 'gradient_end': '#efa73d', 'label': 'Typedef', 'category': 'Data'},
167
+ 'pointer': {'color': '#ff7043', 'icon': '*', 'gradient_start': '#ff8053', 'gradient_end': '#ef6033', 'label': 'Pointer', 'category': 'Data'},
168
+
169
+ # Organization
170
+ 'namespace': {'color': '#607d8b', 'icon': 'N', 'gradient_start': '#708d9b', 'gradient_end': '#506d7b', 'label': 'Namespace', 'category': 'Organization'},
171
+ 'module': {'color': '#78909c', 'icon': 'M', 'gradient_start': '#88a0ac', 'gradient_end': '#68808c', 'label': 'Module', 'category': 'Organization'},
172
+ 'package': {'color': '#90a4ae', 'icon': 'P', 'gradient_start': '#a0b4be', 'gradient_end': '#80949e', 'label': 'Package', 'category': 'Organization'},
173
+ 'file': {'color': '#b0bec5', 'icon': 'F', 'gradient_start': '#c0ced5', 'gradient_end': '#a0aeb5', 'label': 'File', 'category': 'Organization'},
174
+ 'folder': {'color': '#8d6e63', 'icon': 'D', 'gradient_start': '#9d7e73', 'gradient_end': '#7d5e53', 'label': 'Folder', 'category': 'Organization'},
175
+ 'header': {'color': '#a1887f', 'icon': 'H', 'gradient_start': '#b1988f', 'gradient_end': '#91786f', 'label': 'Header', 'category': 'Organization'},
176
+
177
+ # Flow
178
+ 'condition': {'color': '#ef5350', 'icon': '?', 'gradient_start': '#ff6360', 'gradient_end': '#df4340', 'label': 'Condition', 'category': 'Flow'},
179
+ 'loop': {'color': '#e57373', 'icon': '⟳', 'gradient_start': '#f58383', 'gradient_end': '#d56363', 'label': 'Loop', 'category': 'Flow'},
180
+ 'exception': {'color': '#f44336', 'icon': '!', 'gradient_start': '#ff5346', 'gradient_end': '#e43326', 'label': 'Exception', 'category': 'Flow'},
181
+ 'async': {'color': '#42a5f5', 'icon': '⚡', 'gradient_start': '#52b5ff', 'gradient_end': '#3295e5', 'label': 'Async', 'category': 'Flow'},
182
+ 'callback': {'color': '#5c6bc0', 'icon': '↩', 'gradient_start': '#6c7bd0', 'gradient_end': '#4c5bb0', 'label': 'Callback', 'category': 'Flow'},
183
+ 'event': {'color': '#7e57c2', 'icon': '⚑', 'gradient_start': '#8e67d2', 'gradient_end': '#6e47b2', 'label': 'Event', 'category': 'Flow'},
184
+ }
185
+
186
+
187
+ # ============================================================================
188
+ # Data Structures
189
+ # ============================================================================
190
+
191
+ @dataclass
192
+ class PortData:
193
+ """Data for a connection port on a node."""
194
+ id: str = ""
195
+ port_type: str = "bidirectional" # input, output, bidirectional
196
+ data_type: str = "any" # any, int, float, string, object, function
197
+ name: str = ""
198
+
199
+ def __post_init__(self):
200
+ if not self.id:
201
+ self.id = str(uuid.uuid4())[:8]
202
+
203
+
204
+ @dataclass
205
+ class NodeData:
206
+ """Data for a visual node in the CodeMaker"""
207
+ id: str = ""
208
+ node_type: str = "class"
209
+ name: str = ""
210
+ description: str = ""
211
+ x: float = 0.0
212
+ y: float = 0.0
213
+ width: float = 200.0
214
+ height: float = 100.0
215
+ color: str = ""
216
+ connections: List[str] = field(default_factory=list)
217
+ properties: Dict[str, Any] = field(default_factory=dict)
218
+ ports: List[Dict] = field(default_factory=list)
219
+ group_id: str = ""
220
+ created_at: str = ""
221
+ updated_at: str = ""
222
+
223
+ def __post_init__(self):
224
+ if not self.id:
225
+ self.id = str(uuid.uuid4())[:8]
226
+ if not self.color:
227
+ self.color = NODE_TYPES.get(self.node_type, {}).get('color', '#4a9eff')
228
+ if not self.created_at:
229
+ self.created_at = datetime.now().isoformat()
230
+ self.updated_at = datetime.now().isoformat()
231
+
232
+
233
+ @dataclass
234
+ class ConnectionData:
235
+ """Data for a connection between nodes"""
236
+ id: str = ""
237
+ start_node_id: str = ""
238
+ end_node_id: str = ""
239
+ start_port: str = ""
240
+ end_port: str = ""
241
+ label: str = ""
242
+ color: str = "#4a9eff"
243
+ style: str = "solid" # solid, dashed, dotted
244
+
245
+ def __post_init__(self):
246
+ if not self.id:
247
+ self.id = str(uuid.uuid4())[:8]
248
+
249
+
250
+ @dataclass
251
+ class GroupData:
252
+ """Data for a node group"""
253
+ id: str = ""
254
+ name: str = ""
255
+ color: str = "#607d8b"
256
+ node_ids: List[str] = field(default_factory=list)
257
+ collapsed: bool = False
258
+ x: float = 0.0
259
+ y: float = 0.0
260
+ width: float = 300.0
261
+ height: float = 200.0
262
+
263
+ def __post_init__(self):
264
+ if not self.id:
265
+ self.id = str(uuid.uuid4())[:8]
266
+
267
+
268
+ @dataclass
269
+ class TemplateData:
270
+ """Data for a node template"""
271
+ name: str = ""
272
+ category: str = "Custom"
273
+ description: str = ""
274
+ nodes: List[Dict] = field(default_factory=list)
275
+ connections: List[Dict] = field(default_factory=list)
276
+ code_template: str = ""
277
+
278
+
279
+ @dataclass
280
+ class MapData:
281
+ """Data for a complete mindmap file"""
282
+ name: str = ""
283
+ description: str = ""
284
+ nodes: List[NodeData] = field(default_factory=list)
285
+ connections: List[ConnectionData] = field(default_factory=list)
286
+ groups: List[GroupData] = field(default_factory=list)
287
+ viewport_x: float = 0.0
288
+ viewport_y: float = 0.0
289
+ viewport_zoom: float = 1.0
290
+ grid_visible: bool = True
291
+ snap_to_grid: bool = False
292
+ created_at: str = ""
293
+ updated_at: str = ""
294
+
295
+ def __post_init__(self):
296
+ if not self.created_at:
297
+ self.created_at = datetime.now().isoformat()
298
+ self.updated_at = datetime.now().isoformat()
299
+
300
+ def to_dict(self) -> dict:
301
+ return {
302
+ 'name': self.name,
303
+ 'description': self.description,
304
+ 'nodes': [asdict(n) for n in self.nodes],
305
+ 'connections': [asdict(c) for c in self.connections],
306
+ 'groups': [asdict(g) for g in self.groups],
307
+ 'viewport_x': self.viewport_x,
308
+ 'viewport_y': self.viewport_y,
309
+ 'viewport_zoom': self.viewport_zoom,
310
+ 'grid_visible': self.grid_visible,
311
+ 'snap_to_grid': self.snap_to_grid,
312
+ 'created_at': self.created_at,
313
+ 'updated_at': datetime.now().isoformat()
314
+ }
315
+
316
+ @classmethod
317
+ def from_dict(cls, data: dict) -> 'MapData':
318
+ nodes = [NodeData(**n) for n in data.get('nodes', [])]
319
+ connections = [ConnectionData(**c) for c in data.get('connections', [])]
320
+ groups = [GroupData(**g) for g in data.get('groups', [])]
321
+ return cls(
322
+ name=data.get('name', 'Untitled'),
323
+ description=data.get('description', ''),
324
+ nodes=nodes,
325
+ connections=connections,
326
+ groups=groups,
327
+ viewport_x=data.get('viewport_x', 0.0),
328
+ viewport_y=data.get('viewport_y', 0.0),
329
+ viewport_zoom=data.get('viewport_zoom', 1.0),
330
+ grid_visible=data.get('grid_visible', True),
331
+ snap_to_grid=data.get('snap_to_grid', False),
332
+ created_at=data.get('created_at', ''),
333
+ updated_at=data.get('updated_at', '')
334
+ )
335
+
336
+
337
+ if PYQT_AVAILABLE:
338
+
339
+ # ========================================================================
340
+ # Undo/Redo Commands
341
+ # ========================================================================
342
+
343
+ class AddNodeCommand(QUndoCommand):
344
+ """Undoable command for adding a node."""
345
+ def __init__(self, canvas, node_data: NodeData, description="Add Node"):
346
+ super().__init__(description)
347
+ self.canvas = canvas
348
+ self.node_data = node_data
349
+ self.node_id = node_data.id
350
+
351
+ def redo(self):
352
+ self.canvas._add_node_internal(self.node_data)
353
+
354
+ def undo(self):
355
+ if self.node_id in self.canvas.nodes:
356
+ self.canvas._delete_node_internal(self.canvas.nodes[self.node_id])
357
+
358
+
359
+ class DeleteNodeCommand(QUndoCommand):
360
+ """Undoable command for deleting a node."""
361
+ def __init__(self, canvas, node, description="Delete Node"):
362
+ super().__init__(description)
363
+ self.canvas = canvas
364
+ self.node_data = copy.deepcopy(node.node_data)
365
+ self.connections_data = []
366
+ for conn in node.connections:
367
+ self.connections_data.append(copy.deepcopy(conn.connection_data))
368
+
369
+ def redo(self):
370
+ if self.node_data.id in self.canvas.nodes:
371
+ self.canvas._delete_node_internal(self.canvas.nodes[self.node_data.id])
372
+
373
+ def undo(self):
374
+ self.canvas._add_node_internal(self.node_data)
375
+ for conn_data in self.connections_data:
376
+ if conn_data.start_node_id in self.canvas.nodes and conn_data.end_node_id in self.canvas.nodes:
377
+ self.canvas._add_connection_internal(conn_data)
378
+
379
+
380
+ class MoveNodeCommand(QUndoCommand):
381
+ """Undoable command for moving nodes."""
382
+ def __init__(self, canvas, node_id: str, old_pos: QPointF, new_pos: QPointF, description="Move Node"):
383
+ super().__init__(description)
384
+ self.canvas = canvas
385
+ self.node_id = node_id
386
+ self.old_pos = old_pos
387
+ self.new_pos = new_pos
388
+
389
+ def redo(self):
390
+ if self.node_id in self.canvas.nodes:
391
+ node = self.canvas.nodes[self.node_id]
392
+ node.setPos(self.new_pos)
393
+ node.node_data.x = self.new_pos.x()
394
+ node.node_data.y = self.new_pos.y()
395
+ for conn in node.connections:
396
+ conn.update_path()
397
+
398
+ def undo(self):
399
+ if self.node_id in self.canvas.nodes:
400
+ node = self.canvas.nodes[self.node_id]
401
+ node.setPos(self.old_pos)
402
+ node.node_data.x = self.old_pos.x()
403
+ node.node_data.y = self.old_pos.y()
404
+ for conn in node.connections:
405
+ conn.update_path()
406
+
407
+
408
+ class AddConnectionCommand(QUndoCommand):
409
+ """Undoable command for adding a connection."""
410
+ def __init__(self, canvas, conn_data: ConnectionData, description="Add Connection"):
411
+ super().__init__(description)
412
+ self.canvas = canvas
413
+ self.conn_data = conn_data
414
+
415
+ def redo(self):
416
+ self.canvas._add_connection_internal(self.conn_data)
417
+
418
+ def undo(self):
419
+ for conn in self.canvas.connections:
420
+ if conn.connection_data.id == self.conn_data.id:
421
+ self.canvas._delete_connection_internal(conn)
422
+ break
423
+
424
+
425
+ # ========================================================================
426
+ # Animated Button with Sophisticated Hover Effects
427
+ # ========================================================================
428
+
429
+ class AnimatedButton(QPushButton):
430
+ """Modern animated button with gradient hover effects"""
431
+
432
+ def __init__(self, text: str, icon_text: str = "", accent_color: str = None, parent=None):
433
+ super().__init__(parent)
434
+ self._text = text
435
+ self._icon_text = icon_text
436
+ self._accent = QColor(accent_color or THEME['accent_blue'])
437
+ self._hover_progress = 0.0
438
+ self._pressed = False
439
+
440
+ self.setText(f" {icon_text} {text}" if icon_text else f" {text}")
441
+ self.setMinimumHeight(44)
442
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
443
+ self.setFont(QFont(SYSTEM_FONT, 11))
444
+
445
+ self._animation = QVariantAnimation(self)
446
+ self._animation.setDuration(150)
447
+ self._animation.setEasingCurve(QEasingCurve.Type.OutCubic)
448
+ self._animation.valueChanged.connect(self._update_hover)
449
+
450
+ self._update_style()
451
+
452
+ def _update_hover(self, value):
453
+ self._hover_progress = value
454
+ self._update_style()
455
+
456
+ def _update_style(self):
457
+ progress = self._hover_progress
458
+
459
+ bg_base = QColor(THEME['bg_tertiary'])
460
+ bg_hover = self._accent.darker(140)
461
+
462
+ r = int(bg_base.red() + (bg_hover.red() - bg_base.red()) * progress)
463
+ g = int(bg_base.green() + (bg_hover.green() - bg_base.green()) * progress)
464
+ b = int(bg_base.blue() + (bg_hover.blue() - bg_base.blue()) * progress)
465
+
466
+ bg_color = f"rgb({r}, {g}, {b})"
467
+ border_color = self._accent.name() if progress > 0.3 else THEME['border']
468
+
469
+ if self._pressed:
470
+ bg_color = self._accent.name()
471
+ border_color = self._accent.lighter(120).name()
472
+
473
+ self.setStyleSheet(f'''
474
+ QPushButton {{
475
+ background-color: {bg_color};
476
+ border: 1px solid {border_color};
477
+ border-radius: 8px;
478
+ color: {THEME['text_primary']};
479
+ padding: 10px 16px;
480
+ text-align: left;
481
+ font-weight: 500;
482
+ }}
483
+ ''')
484
+
485
+ def enterEvent(self, event):
486
+ self._animation.setStartValue(self._hover_progress)
487
+ self._animation.setEndValue(1.0)
488
+ self._animation.start()
489
+ super().enterEvent(event)
490
+
491
+ def leaveEvent(self, event):
492
+ self._animation.setStartValue(self._hover_progress)
493
+ self._animation.setEndValue(0.0)
494
+ self._animation.start()
495
+ super().leaveEvent(event)
496
+
497
+ def mousePressEvent(self, event):
498
+ self._pressed = True
499
+ self._update_style()
500
+ super().mousePressEvent(event)
501
+
502
+ def mouseReleaseEvent(self, event):
503
+ self._pressed = False
504
+ self._update_style()
505
+ super().mouseReleaseEvent(event)
506
+
507
+
508
+ # ========================================================================
509
+ # Visual Node with Professional Design
510
+ # ========================================================================
511
+
512
+ class VisualNode(QGraphicsRectItem):
513
+ """A professional visual node with gradients, shadows, and animations"""
514
+
515
+ def __init__(self, node_data: NodeData, parent=None):
516
+ super().__init__(parent)
517
+ self.node_data = node_data
518
+ self.connections: List['ConnectionLine'] = []
519
+ self._hover = False
520
+ self._selected = False
521
+ self._drag_start_pos = None
522
+
523
+ self.setRect(0, 0, node_data.width, node_data.height)
524
+ self.setPos(node_data.x, node_data.y)
525
+
526
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
527
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
528
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
529
+ self.setAcceptHoverEvents(True)
530
+ self.setZValue(1)
531
+
532
+ self.node_theme = NODE_TYPES.get(node_data.node_type, NODE_TYPES['class'])
533
+ self.base_color = QColor(self.node_theme['color'])
534
+
535
+ self._create_visual_elements()
536
+ self._apply_shadow()
537
+
538
+ def _create_visual_elements(self):
539
+ """Create all visual elements for the node"""
540
+ rect = self.rect()
541
+ w, h = rect.width(), rect.height()
542
+
543
+ gradient = QLinearGradient(0, 0, 0, h)
544
+ gradient.setColorAt(0, QColor(self.node_theme['gradient_start']))
545
+ gradient.setColorAt(0.3, QColor(self.node_theme['color']))
546
+ gradient.setColorAt(1, QColor(self.node_theme['gradient_end']))
547
+
548
+ self.setBrush(QBrush(gradient))
549
+ self.setPen(QPen(self.base_color.darker(120), 2))
550
+
551
+ self.header = QGraphicsRectItem(0, 0, w, 32, self)
552
+ header_gradient = QLinearGradient(0, 0, 0, 32)
553
+ header_gradient.setColorAt(0, QColor(255, 255, 255, 30))
554
+ header_gradient.setColorAt(1, QColor(0, 0, 0, 30))
555
+ self.header.setBrush(QBrush(header_gradient))
556
+ self.header.setPen(QPen(Qt.PenStyle.NoPen))
557
+
558
+ icon_size = 26
559
+ self.icon_bg = QGraphicsEllipseItem(8, 4, icon_size, icon_size, self)
560
+ self.icon_bg.setBrush(QBrush(QColor(THEME['bg_dark'])))
561
+ self.icon_bg.setPen(QPen(self.base_color.lighter(120), 1.5))
562
+
563
+ icon_text = self.node_theme['icon']
564
+ self.icon_label = QGraphicsTextItem(icon_text, self)
565
+ self.icon_label.setDefaultTextColor(self.base_color.lighter(130))
566
+ icon_font = QFont(MONO_FONT, 10, QFont.Weight.Bold)
567
+ self.icon_label.setFont(icon_font)
568
+ icon_rect = self.icon_label.boundingRect()
569
+ self.icon_label.setPos(
570
+ 8 + (icon_size - icon_rect.width()) / 2,
571
+ 4 + (icon_size - icon_rect.height()) / 2
572
+ )
573
+
574
+ self.name_label = QGraphicsTextItem(self.node_data.name, self)
575
+ self.name_label.setDefaultTextColor(QColor("#ffffff"))
576
+ self.name_label.setFont(QFont(SYSTEM_FONT, 11, QFont.Weight.Bold))
577
+ self.name_label.setPos(40, 6)
578
+
579
+ type_text = self.node_theme['label'].upper()
580
+ self.type_label = QGraphicsTextItem(type_text, self)
581
+ self.type_label.setDefaultTextColor(QColor(255, 255, 255, 150))
582
+ self.type_label.setFont(QFont(SYSTEM_FONT, 8))
583
+ type_rect = self.type_label.boundingRect()
584
+ self.type_label.setPos(w - type_rect.width() - 10, 10)
585
+
586
+ if self.node_data.description:
587
+ desc_text = self.node_data.description[:80]
588
+ if len(self.node_data.description) > 80:
589
+ desc_text += "..."
590
+ self.desc_label = QGraphicsTextItem(desc_text, self)
591
+ self.desc_label.setDefaultTextColor(QColor(255, 255, 255, 180))
592
+ self.desc_label.setFont(QFont(SYSTEM_FONT, 9))
593
+ self.desc_label.setTextWidth(w - 20)
594
+ self.desc_label.setPos(10, 38)
595
+
596
+ point_size = 10
597
+ self.connection_points = []
598
+
599
+ right_point = QGraphicsEllipseItem(
600
+ w - point_size/2, h/2 - point_size/2,
601
+ point_size, point_size, self
602
+ )
603
+ right_point.setBrush(QBrush(QColor(THEME['bg_dark'])))
604
+ right_point.setPen(QPen(self.base_color, 2))
605
+ right_point.setZValue(2)
606
+ self.connection_points.append(('right', right_point))
607
+
608
+ left_point = QGraphicsEllipseItem(
609
+ -point_size/2, h/2 - point_size/2,
610
+ point_size, point_size, self
611
+ )
612
+ left_point.setBrush(QBrush(QColor(THEME['bg_dark'])))
613
+ left_point.setPen(QPen(self.base_color, 2))
614
+ left_point.setZValue(2)
615
+ self.connection_points.append(('left', left_point))
616
+
617
+ self.bottom_line = QGraphicsRectItem(10, h - 4, w - 20, 2, self)
618
+ self.bottom_line.setBrush(QBrush(self.base_color.lighter(130)))
619
+ self.bottom_line.setPen(QPen(Qt.PenStyle.NoPen))
620
+
621
+ def _apply_shadow(self):
622
+ """Apply drop shadow effect"""
623
+ shadow = QGraphicsDropShadowEffect()
624
+ shadow.setBlurRadius(25)
625
+ shadow.setColor(QColor(0, 0, 0, 120))
626
+ shadow.setOffset(4, 4)
627
+ self.setGraphicsEffect(shadow)
628
+
629
+ def itemChange(self, change, value):
630
+ """Handle position changes"""
631
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
632
+ pos = self.pos()
633
+ self.node_data.x = pos.x()
634
+ self.node_data.y = pos.y()
635
+ self.node_data.updated_at = datetime.now().isoformat()
636
+ for conn in self.connections:
637
+ conn.update_path()
638
+ elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
639
+ self._selected = value
640
+ self._update_visual_state()
641
+ return super().itemChange(change, value)
642
+
643
+ def _update_visual_state(self):
644
+ """Update visual appearance based on state"""
645
+ if self._selected:
646
+ pen_width = 3
647
+ pen_color = self.base_color.lighter(150)
648
+ elif self._hover:
649
+ pen_width = 2.5
650
+ pen_color = self.base_color.lighter(130)
651
+ else:
652
+ pen_width = 2
653
+ pen_color = self.base_color.darker(120)
654
+
655
+ self.setPen(QPen(pen_color, pen_width))
656
+
657
+ def get_connection_point(self, side: str) -> QPointF:
658
+ """Get the connection point position for a side"""
659
+ rect = self.sceneBoundingRect()
660
+ if side == 'right':
661
+ return QPointF(rect.right(), rect.center().y())
662
+ elif side == 'left':
663
+ return QPointF(rect.left(), rect.center().y())
664
+ elif side == 'top':
665
+ return QPointF(rect.center().x(), rect.top())
666
+ elif side == 'bottom':
667
+ return QPointF(rect.center().x(), rect.bottom())
668
+ return rect.center()
669
+
670
+ def get_nearest_connection_point(self, target: QPointF) -> Tuple[str, QPointF]:
671
+ """Get the nearest connection point to a target"""
672
+ sides = ['left', 'right', 'top', 'bottom']
673
+ min_dist = float('inf')
674
+ nearest = ('right', self.get_connection_point('right'))
675
+
676
+ for side in sides:
677
+ point = self.get_connection_point(side)
678
+ dist = (point.x() - target.x())**2 + (point.y() - target.y())**2
679
+ if dist < min_dist:
680
+ min_dist = dist
681
+ nearest = (side, point)
682
+
683
+ return nearest
684
+
685
+ def hoverEnterEvent(self, event):
686
+ self._hover = True
687
+ self._update_visual_state()
688
+ super().hoverEnterEvent(event)
689
+
690
+ def hoverLeaveEvent(self, event):
691
+ self._hover = False
692
+ self._update_visual_state()
693
+ super().hoverLeaveEvent(event)
694
+
695
+ def mousePressEvent(self, event):
696
+ self._drag_start_pos = self.pos()
697
+ super().mousePressEvent(event)
698
+
699
+ def mouseReleaseEvent(self, event):
700
+ if self._drag_start_pos and self._drag_start_pos != self.pos():
701
+ canvas = self.scene().views()[0] if self.scene() and self.scene().views() else None
702
+ if canvas and hasattr(canvas, 'undo_stack'):
703
+ cmd = MoveNodeCommand(canvas, self.node_data.id, self._drag_start_pos, self.pos())
704
+ canvas.undo_stack.push(cmd)
705
+ self._drag_start_pos = None
706
+ super().mouseReleaseEvent(event)
707
+
708
+ def update_name(self, name: str):
709
+ """Update the displayed name"""
710
+ self.node_data.name = name
711
+ self.name_label.setPlainText(name)
712
+ self.node_data.updated_at = datetime.now().isoformat()
713
+
714
+ def update_description(self, desc: str):
715
+ """Update the description"""
716
+ self.node_data.description = desc
717
+ if hasattr(self, 'desc_label'):
718
+ display_text = desc[:80] + "..." if len(desc) > 80 else desc
719
+ self.desc_label.setPlainText(display_text)
720
+ self.node_data.updated_at = datetime.now().isoformat()
721
+
722
+
723
+ # ========================================================================
724
+ # Connection Line with Bezier Curves and Arrows
725
+ # ========================================================================
726
+
727
+ class ConnectionLine(QGraphicsPathItem):
728
+ """A sophisticated connection line with bezier curves and arrow head"""
729
+
730
+ def __init__(self, start_node: VisualNode, end_node: VisualNode,
731
+ connection_data: ConnectionData = None, parent=None):
732
+ super().__init__(parent)
733
+ self.start_node = start_node
734
+ self.end_node = end_node
735
+
736
+ if connection_data:
737
+ self.connection_data = connection_data
738
+ else:
739
+ self.connection_data = ConnectionData(
740
+ id=str(uuid.uuid4())[:8],
741
+ start_node_id=start_node.node_data.id,
742
+ end_node_id=end_node.node_data.id
743
+ )
744
+
745
+ start_node.connections.append(self)
746
+ end_node.connections.append(self)
747
+
748
+ self.line_color = QColor(self.connection_data.color)
749
+ self._hover = False
750
+ self._setup_style()
751
+ self.setZValue(0)
752
+ self.setAcceptHoverEvents(True)
753
+
754
+ self.arrow_head = QGraphicsPolygonItem(self)
755
+ self.arrow_head.setBrush(QBrush(self.line_color))
756
+ self.arrow_head.setPen(QPen(Qt.PenStyle.NoPen))
757
+
758
+ self.label_item = None
759
+ if self.connection_data.label:
760
+ self._create_label()
761
+
762
+ self.update_path()
763
+
764
+ def _setup_style(self):
765
+ """Setup pen style based on connection data"""
766
+ pen = QPen(self.line_color, 2.5)
767
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
768
+
769
+ if self.connection_data.style == 'dashed':
770
+ pen.setStyle(Qt.PenStyle.DashLine)
771
+ elif self.connection_data.style == 'dotted':
772
+ pen.setStyle(Qt.PenStyle.DotLine)
773
+ else:
774
+ pen.setStyle(Qt.PenStyle.SolidLine)
775
+
776
+ self.setPen(pen)
777
+
778
+ def _create_label(self):
779
+ """Create label for the connection"""
780
+ self.label_item = QGraphicsTextItem(self.connection_data.label, self)
781
+ self.label_item.setDefaultTextColor(QColor(THEME['text_secondary']))
782
+ self.label_item.setFont(QFont(SYSTEM_FONT, 9))
783
+
784
+ def update_path(self):
785
+ """Update the bezier curve path"""
786
+ end_center = self.end_node.sceneBoundingRect().center()
787
+ start_side, start_point = self.start_node.get_nearest_connection_point(end_center)
788
+
789
+ start_center = self.start_node.sceneBoundingRect().center()
790
+ end_side, end_point = self.end_node.get_nearest_connection_point(start_center)
791
+
792
+ dx = end_point.x() - start_point.x()
793
+ dy = end_point.y() - start_point.y()
794
+ dist = math.sqrt(dx*dx + dy*dy)
795
+ tension = min(dist * 0.4, 150)
796
+
797
+ def get_direction(side):
798
+ if side == 'right':
799
+ return QPointF(1, 0)
800
+ elif side == 'left':
801
+ return QPointF(-1, 0)
802
+ elif side == 'top':
803
+ return QPointF(0, -1)
804
+ else:
805
+ return QPointF(0, 1)
806
+
807
+ start_dir = get_direction(start_side)
808
+ end_dir = get_direction(end_side)
809
+
810
+ ctrl1 = QPointF(
811
+ start_point.x() + start_dir.x() * tension,
812
+ start_point.y() + start_dir.y() * tension
813
+ )
814
+ ctrl2 = QPointF(
815
+ end_point.x() + end_dir.x() * tension,
816
+ end_point.y() + end_dir.y() * tension
817
+ )
818
+
819
+ path = QPainterPath()
820
+ path.moveTo(start_point)
821
+ path.cubicTo(ctrl1, ctrl2, end_point)
822
+ self.setPath(path)
823
+
824
+ self._update_arrow(end_point, ctrl2)
825
+
826
+ if self.label_item:
827
+ mid_point = path.pointAtPercent(0.5)
828
+ label_rect = self.label_item.boundingRect()
829
+ self.label_item.setPos(
830
+ mid_point.x() - label_rect.width() / 2,
831
+ mid_point.y() - label_rect.height() / 2
832
+ )
833
+
834
+ def _update_arrow(self, tip: QPointF, control: QPointF):
835
+ """Update arrow head at the end of the line"""
836
+ dx = tip.x() - control.x()
837
+ dy = tip.y() - control.y()
838
+ length = math.sqrt(dx*dx + dy*dy)
839
+
840
+ if length > 0:
841
+ dx /= length
842
+ dy /= length
843
+
844
+ arrow_size = 12
845
+ angle = math.pi / 6
846
+
847
+ p1 = tip
848
+ p2 = QPointF(
849
+ tip.x() - arrow_size * (dx * math.cos(angle) - dy * math.sin(angle)),
850
+ tip.y() - arrow_size * (dy * math.cos(angle) + dx * math.sin(angle))
851
+ )
852
+ p3 = QPointF(
853
+ tip.x() - arrow_size * (dx * math.cos(angle) + dy * math.sin(angle)),
854
+ tip.y() - arrow_size * (dy * math.cos(angle) - dx * math.sin(angle))
855
+ )
856
+
857
+ self.arrow_head.setPolygon(QPolygonF([p1, p2, p3]))
858
+
859
+ def hoverEnterEvent(self, event):
860
+ self._hover = True
861
+ pen = self.pen()
862
+ pen.setWidth(4)
863
+ pen.setColor(self.line_color.lighter(130))
864
+ self.setPen(pen)
865
+ self.arrow_head.setBrush(QBrush(self.line_color.lighter(130)))
866
+ super().hoverEnterEvent(event)
867
+
868
+ def hoverLeaveEvent(self, event):
869
+ self._hover = False
870
+ self._setup_style()
871
+ self.arrow_head.setBrush(QBrush(self.line_color))
872
+ super().hoverLeaveEvent(event)
873
+
874
+ def remove(self):
875
+ """Remove this connection"""
876
+ if self in self.start_node.connections:
877
+ self.start_node.connections.remove(self)
878
+ if self in self.end_node.connections:
879
+ self.end_node.connections.remove(self)
880
+
881
+
882
+ # ========================================================================
883
+ # Node Properties Dialog
884
+ # ========================================================================
885
+
886
+ class NodePropertiesDialog(QDialog):
887
+ """Dialog for editing node properties"""
888
+
889
+ def __init__(self, node: VisualNode, parent=None):
890
+ super().__init__(parent)
891
+ self.node = node
892
+ self.setWindowTitle("Node Properties")
893
+ self.setMinimumSize(450, 400)
894
+ self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
895
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
896
+
897
+ self._setup_ui()
898
+
899
+ def _setup_ui(self):
900
+ container = QFrame(self)
901
+ container.setStyleSheet(f'''
902
+ QFrame {{
903
+ background-color: {THEME['bg_secondary']};
904
+ border: 1px solid {THEME['border']};
905
+ border-radius: 12px;
906
+ }}
907
+ QLabel {{
908
+ color: {THEME['text_primary']};
909
+ font-size: 12px;
910
+ }}
911
+ QLineEdit, QTextEdit, QComboBox {{
912
+ background-color: {THEME['bg_tertiary']};
913
+ border: 1px solid {THEME['border']};
914
+ border-radius: 6px;
915
+ padding: 8px;
916
+ color: {THEME['text_primary']};
917
+ font-size: 12px;
918
+ }}
919
+ QLineEdit:focus, QTextEdit:focus {{
920
+ border: 1px solid {THEME['accent_blue']};
921
+ }}
922
+ ''')
923
+
924
+ main_layout = QVBoxLayout(self)
925
+ main_layout.setContentsMargins(0, 0, 0, 0)
926
+ main_layout.addWidget(container)
927
+
928
+ layout = QVBoxLayout(container)
929
+ layout.setContentsMargins(20, 20, 20, 20)
930
+ layout.setSpacing(16)
931
+
932
+ header = QLabel("Edit Node")
933
+ header.setFont(QFont(SYSTEM_FONT, 14, QFont.Weight.Bold))
934
+ header.setStyleSheet(f'color: {THEME["accent_blue"]};')
935
+ layout.addWidget(header)
936
+
937
+ form = QFormLayout()
938
+ form.setSpacing(12)
939
+
940
+ self.name_edit = QLineEdit(self.node.node_data.name)
941
+ form.addRow("Name:", self.name_edit)
942
+
943
+ self.type_combo = QComboBox()
944
+ for ntype in NODE_TYPES.keys():
945
+ self.type_combo.addItem(NODE_TYPES[ntype]['label'], ntype)
946
+ idx = self.type_combo.findData(self.node.node_data.node_type)
947
+ if idx >= 0:
948
+ self.type_combo.setCurrentIndex(idx)
949
+ form.addRow("Type:", self.type_combo)
950
+
951
+ self.desc_edit = QTextEdit()
952
+ self.desc_edit.setPlainText(self.node.node_data.description)
953
+ self.desc_edit.setMaximumHeight(100)
954
+ form.addRow("Description:", self.desc_edit)
955
+
956
+ layout.addLayout(form)
957
+
958
+ btn_layout = QHBoxLayout()
959
+ btn_layout.addStretch()
960
+
961
+ cancel_btn = QPushButton("Cancel")
962
+ cancel_btn.setStyleSheet(f'''
963
+ QPushButton {{
964
+ background-color: {THEME['bg_tertiary']};
965
+ border: 1px solid {THEME['border']};
966
+ border-radius: 6px;
967
+ padding: 10px 24px;
968
+ color: {THEME['text_primary']};
969
+ }}
970
+ QPushButton:hover {{
971
+ background-color: {THEME['bg_hover']};
972
+ }}
973
+ ''')
974
+ cancel_btn.clicked.connect(self.reject)
975
+ btn_layout.addWidget(cancel_btn)
976
+
977
+ save_btn = QPushButton("Save")
978
+ save_btn.setStyleSheet(f'''
979
+ QPushButton {{
980
+ background-color: {THEME['accent_blue']};
981
+ border: none;
982
+ border-radius: 6px;
983
+ padding: 10px 24px;
984
+ color: white;
985
+ font-weight: bold;
986
+ }}
987
+ QPushButton:hover {{
988
+ background-color: {QColor(THEME['accent_blue']).lighter(110).name()};
989
+ }}
990
+ ''')
991
+ save_btn.clicked.connect(self.accept)
992
+ btn_layout.addWidget(save_btn)
993
+
994
+ layout.addLayout(btn_layout)
995
+
996
+ def get_values(self) -> Tuple[str, str, str]:
997
+ return (
998
+ self.name_edit.text(),
999
+ self.desc_edit.toPlainText(),
1000
+ self.type_combo.currentData()
1001
+ )
1002
+
1003
+
1004
+ # ========================================================================
1005
+ # Code Generator
1006
+ # ========================================================================
1007
+
1008
+ class CodeGenerator:
1009
+ """Generates C++ and Python code from nodes."""
1010
+
1011
+ @staticmethod
1012
+ def generate_cpp_header(node: NodeData) -> str:
1013
+ """Generate C++ header code for a node."""
1014
+ lines = []
1015
+ ntype = node.node_type
1016
+ name = node.name.replace(' ', '_')
1017
+
1018
+ if ntype == 'class':
1019
+ lines.append(f"class {name} {{")
1020
+ lines.append("public:")
1021
+ lines.append(f" {name}();")
1022
+ lines.append(f" ~{name}();")
1023
+ lines.append("")
1024
+ lines.append("private:")
1025
+ lines.append("};")
1026
+ elif ntype == 'struct':
1027
+ lines.append(f"struct {name} {{")
1028
+ lines.append("};")
1029
+ elif ntype == 'enum':
1030
+ lines.append(f"enum class {name} {{")
1031
+ lines.append(" Value1,")
1032
+ lines.append(" Value2,")
1033
+ lines.append("};")
1034
+ elif ntype == 'function':
1035
+ lines.append(f"void {name}();")
1036
+ elif ntype == 'interface':
1037
+ lines.append(f"class I{name} {{")
1038
+ lines.append("public:")
1039
+ lines.append(f" virtual ~I{name}() = default;")
1040
+ lines.append("};")
1041
+ elif ntype == 'namespace':
1042
+ lines.append(f"namespace {name} {{")
1043
+ lines.append("")
1044
+ lines.append(f"}} // namespace {name}")
1045
+ else:
1046
+ lines.append(f"// {node.node_type}: {name}")
1047
+
1048
+ return '\n'.join(lines)
1049
+
1050
+ @staticmethod
1051
+ def generate_cpp_source(node: NodeData) -> str:
1052
+ """Generate C++ source code for a node."""
1053
+ lines = []
1054
+ ntype = node.node_type
1055
+ name = node.name.replace(' ', '_')
1056
+
1057
+ if ntype == 'class':
1058
+ lines.append(f"#include \"{name}.h\"")
1059
+ lines.append("")
1060
+ lines.append(f"{name}::{name}() {{")
1061
+ lines.append("}")
1062
+ lines.append("")
1063
+ lines.append(f"{name}::~{name}() {{")
1064
+ lines.append("}")
1065
+ elif ntype == 'function':
1066
+ lines.append(f"void {name}() {{")
1067
+ lines.append(" // TODO: Implement")
1068
+ lines.append("}")
1069
+ else:
1070
+ lines.append(f"// Implementation for {name}")
1071
+
1072
+ return '\n'.join(lines)
1073
+
1074
+ @staticmethod
1075
+ def generate_python(node: NodeData) -> str:
1076
+ """Generate Python code for a node."""
1077
+ lines = []
1078
+ ntype = node.node_type
1079
+ name = node.name.replace(' ', '_')
1080
+
1081
+ if ntype in ('class', 'struct'):
1082
+ lines.append(f"class {name}:")
1083
+ lines.append(' """')
1084
+ if node.description:
1085
+ lines.append(f" {node.description}")
1086
+ lines.append(' """')
1087
+ lines.append("")
1088
+ lines.append(" def __init__(self):")
1089
+ lines.append(" pass")
1090
+ elif ntype == 'function':
1091
+ lines.append(f"def {name}():")
1092
+ lines.append(' """')
1093
+ if node.description:
1094
+ lines.append(f" {node.description}")
1095
+ lines.append(' """')
1096
+ lines.append(" pass")
1097
+ elif ntype == 'enum':
1098
+ lines.append("from enum import Enum, auto")
1099
+ lines.append("")
1100
+ lines.append(f"class {name}(Enum):")
1101
+ lines.append(" VALUE1 = auto()")
1102
+ lines.append(" VALUE2 = auto()")
1103
+ elif ntype == 'interface':
1104
+ lines.append("from abc import ABC, abstractmethod")
1105
+ lines.append("")
1106
+ lines.append(f"class {name}(ABC):")
1107
+ lines.append(' """Abstract base class."""')
1108
+ lines.append("")
1109
+ lines.append(" @abstractmethod")
1110
+ lines.append(" def method(self):")
1111
+ lines.append(" pass")
1112
+ else:
1113
+ lines.append(f"# {node.node_type}: {name}")
1114
+
1115
+ return '\n'.join(lines)
1116
+
1117
+
1118
+ # ========================================================================
1119
+ # Code Preview Dialog
1120
+ # ========================================================================
1121
+
1122
+ class CodePreviewDialog(QDialog):
1123
+ """Dialog for previewing generated code."""
1124
+
1125
+ def __init__(self, node: VisualNode, parent=None):
1126
+ super().__init__(parent)
1127
+ self.node = node
1128
+ self.setWindowTitle("Code Preview")
1129
+ self.setMinimumSize(600, 500)
1130
+ self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
1131
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
1132
+ self._setup_ui()
1133
+
1134
+ def _setup_ui(self):
1135
+ container = QFrame(self)
1136
+ container.setStyleSheet(f'''
1137
+ QFrame {{
1138
+ background-color: {THEME['bg_secondary']};
1139
+ border: 1px solid {THEME['border']};
1140
+ border-radius: 12px;
1141
+ }}
1142
+ ''')
1143
+
1144
+ main_layout = QVBoxLayout(self)
1145
+ main_layout.setContentsMargins(0, 0, 0, 0)
1146
+ main_layout.addWidget(container)
1147
+
1148
+ layout = QVBoxLayout(container)
1149
+ layout.setContentsMargins(20, 20, 20, 20)
1150
+
1151
+ header = QHBoxLayout()
1152
+ title = QLabel("Code Preview")
1153
+ title.setFont(QFont(SYSTEM_FONT, 14, QFont.Weight.Bold))
1154
+ title.setStyleSheet(f'color: {THEME["accent_blue"]};')
1155
+ header.addWidget(title)
1156
+ header.addStretch()
1157
+
1158
+ close_btn = QPushButton("×")
1159
+ close_btn.setFixedSize(28, 28)
1160
+ close_btn.setStyleSheet(f'''
1161
+ QPushButton {{
1162
+ background: transparent;
1163
+ color: {THEME['text_secondary']};
1164
+ font-size: 18px;
1165
+ border: none;
1166
+ }}
1167
+ QPushButton:hover {{
1168
+ color: {THEME['accent_red']};
1169
+ }}
1170
+ ''')
1171
+ close_btn.clicked.connect(self.close)
1172
+ header.addWidget(close_btn)
1173
+ layout.addLayout(header)
1174
+
1175
+ tabs = QTabWidget()
1176
+ tabs.setStyleSheet(f'''
1177
+ QTabWidget::pane {{
1178
+ border: 1px solid {THEME['border']};
1179
+ border-radius: 6px;
1180
+ background: {THEME['bg_tertiary']};
1181
+ }}
1182
+ QTabBar::tab {{
1183
+ background: {THEME['bg_tertiary']};
1184
+ color: {THEME['text_secondary']};
1185
+ padding: 10px 20px;
1186
+ border-top-left-radius: 6px;
1187
+ border-top-right-radius: 6px;
1188
+ }}
1189
+ QTabBar::tab:selected {{
1190
+ background: {THEME['accent_blue']};
1191
+ color: white;
1192
+ }}
1193
+ ''')
1194
+
1195
+ cpp_header = QPlainTextEdit()
1196
+ cpp_header.setPlainText(CodeGenerator.generate_cpp_header(self.node.node_data))
1197
+ cpp_header.setFont(QFont(MONO_FONT, 11))
1198
+ cpp_header.setStyleSheet(f'''
1199
+ QPlainTextEdit {{
1200
+ background: {THEME['bg_dark']};
1201
+ color: {THEME['text_primary']};
1202
+ border: none;
1203
+ padding: 10px;
1204
+ }}
1205
+ ''')
1206
+ tabs.addTab(cpp_header, "C++ Header")
1207
+
1208
+ cpp_source = QPlainTextEdit()
1209
+ cpp_source.setPlainText(CodeGenerator.generate_cpp_source(self.node.node_data))
1210
+ cpp_source.setFont(QFont(MONO_FONT, 11))
1211
+ cpp_source.setStyleSheet(cpp_header.styleSheet())
1212
+ tabs.addTab(cpp_source, "C++ Source")
1213
+
1214
+ python_code = QPlainTextEdit()
1215
+ python_code.setPlainText(CodeGenerator.generate_python(self.node.node_data))
1216
+ python_code.setFont(QFont(MONO_FONT, 11))
1217
+ python_code.setStyleSheet(cpp_header.styleSheet())
1218
+ tabs.addTab(python_code, "Python")
1219
+
1220
+ layout.addWidget(tabs)
1221
+
1222
+ btn_layout = QHBoxLayout()
1223
+ btn_layout.addStretch()
1224
+
1225
+ copy_btn = QPushButton("Copy to Clipboard")
1226
+ copy_btn.setStyleSheet(f'''
1227
+ QPushButton {{
1228
+ background-color: {THEME['accent_blue']};
1229
+ border: none;
1230
+ border-radius: 6px;
1231
+ padding: 10px 24px;
1232
+ color: white;
1233
+ font-weight: bold;
1234
+ }}
1235
+ ''')
1236
+ copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(
1237
+ tabs.currentWidget().toPlainText()
1238
+ ))
1239
+ btn_layout.addWidget(copy_btn)
1240
+ layout.addLayout(btn_layout)
1241
+
1242
+
1243
+ # ========================================================================
1244
+ # Search Panel
1245
+ # ========================================================================
1246
+
1247
+ class SearchPanel(QFrame):
1248
+ """Panel for searching and filtering nodes."""
1249
+
1250
+ search_requested = pyqtSignal(str)
1251
+ filter_changed = pyqtSignal(str)
1252
+
1253
+ def __init__(self, parent=None):
1254
+ super().__init__(parent)
1255
+ self.setFixedHeight(50)
1256
+ self._setup_ui()
1257
+
1258
+ def _setup_ui(self):
1259
+ self.setStyleSheet(f'''
1260
+ QFrame {{
1261
+ background: {THEME['bg_secondary']};
1262
+ border-bottom: 1px solid {THEME['border']};
1263
+ }}
1264
+ ''')
1265
+
1266
+ layout = QHBoxLayout(self)
1267
+ layout.setContentsMargins(16, 8, 16, 8)
1268
+
1269
+ self.search_input = QLineEdit()
1270
+ self.search_input.setPlaceholderText("Search nodes... (Ctrl+F)")
1271
+ self.search_input.setStyleSheet(f'''
1272
+ QLineEdit {{
1273
+ background: {THEME['bg_tertiary']};
1274
+ border: 1px solid {THEME['border']};
1275
+ border-radius: 6px;
1276
+ padding: 8px 12px;
1277
+ color: {THEME['text_primary']};
1278
+ }}
1279
+ QLineEdit:focus {{
1280
+ border: 1px solid {THEME['accent_blue']};
1281
+ }}
1282
+ ''')
1283
+ self.search_input.textChanged.connect(self.search_requested.emit)
1284
+ layout.addWidget(self.search_input)
1285
+
1286
+ self.filter_combo = QComboBox()
1287
+ self.filter_combo.addItem("All Types", "all")
1288
+ for category, types in NODE_CATEGORIES.items():
1289
+ self.filter_combo.addItem(f"─ {category} ─", f"cat:{category}")
1290
+ for ntype in types:
1291
+ if ntype in NODE_TYPES:
1292
+ self.filter_combo.addItem(f" {NODE_TYPES[ntype]['label']}", ntype)
1293
+ self.filter_combo.setStyleSheet(f'''
1294
+ QComboBox {{
1295
+ background: {THEME['bg_tertiary']};
1296
+ border: 1px solid {THEME['border']};
1297
+ border-radius: 6px;
1298
+ padding: 8px 12px;
1299
+ color: {THEME['text_primary']};
1300
+ min-width: 150px;
1301
+ }}
1302
+ ''')
1303
+ self.filter_combo.currentIndexChanged.connect(
1304
+ lambda: self.filter_changed.emit(self.filter_combo.currentData())
1305
+ )
1306
+ layout.addWidget(self.filter_combo)
1307
+
1308
+
1309
+ # ========================================================================
1310
+ # Minimap Widget
1311
+ # ========================================================================
1312
+
1313
+ class MinimapWidget(QFrame):
1314
+ """Minimap overview of the canvas."""
1315
+
1316
+ def __init__(self, canvas: 'CodeMakerCanvas', parent=None):
1317
+ super().__init__(parent)
1318
+ self.canvas = canvas
1319
+ self.setFixedSize(200, 150)
1320
+ self._setup_ui()
1321
+
1322
+ def _setup_ui(self):
1323
+ self.setStyleSheet(f'''
1324
+ QFrame {{
1325
+ background: {THEME['bg_secondary']};
1326
+ border: 1px solid {THEME['border']};
1327
+ border-radius: 8px;
1328
+ }}
1329
+ ''')
1330
+
1331
+ def paintEvent(self, event):
1332
+ super().paintEvent(event)
1333
+ painter = QPainter(self)
1334
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1335
+
1336
+ painter.fillRect(self.rect(), QColor(THEME['bg_dark']))
1337
+
1338
+ if not self.canvas.nodes:
1339
+ return
1340
+
1341
+ min_x = min(n.node_data.x for n in self.canvas.nodes.values())
1342
+ max_x = max(n.node_data.x + n.node_data.width for n in self.canvas.nodes.values())
1343
+ min_y = min(n.node_data.y for n in self.canvas.nodes.values())
1344
+ max_y = max(n.node_data.y + n.node_data.height for n in self.canvas.nodes.values())
1345
+
1346
+ content_width = max_x - min_x + 100
1347
+ content_height = max_y - min_y + 100
1348
+
1349
+ scale_x = (self.width() - 20) / max(content_width, 1)
1350
+ scale_y = (self.height() - 20) / max(content_height, 1)
1351
+ scale = min(scale_x, scale_y)
1352
+
1353
+ offset_x = 10 - min_x * scale
1354
+ offset_y = 10 - min_y * scale
1355
+
1356
+ for node in self.canvas.nodes.values():
1357
+ x = node.node_data.x * scale + offset_x
1358
+ y = node.node_data.y * scale + offset_y
1359
+ w = node.node_data.width * scale
1360
+ h = node.node_data.height * scale
1361
+
1362
+ color = QColor(node.node_theme['color'])
1363
+ painter.fillRect(int(x), int(y), int(w), int(h), color)
1364
+
1365
+ viewport_rect = self.canvas.mapToScene(self.canvas.viewport().rect()).boundingRect()
1366
+ vx = viewport_rect.x() * scale + offset_x
1367
+ vy = viewport_rect.y() * scale + offset_y
1368
+ vw = viewport_rect.width() * scale
1369
+ vh = viewport_rect.height() * scale
1370
+
1371
+ painter.setPen(QPen(QColor(THEME['accent_blue']), 2))
1372
+ painter.drawRect(int(vx), int(vy), int(vw), int(vh))
1373
+
1374
+ def update_minimap(self):
1375
+ self.update()
1376
+
1377
+
1378
+ # ========================================================================
1379
+ # CodeMaker Canvas
1380
+ # ========================================================================
1381
+
1382
+ class CodeMakerCanvas(QGraphicsView):
1383
+ """Professional interactive canvas with smooth pan/zoom"""
1384
+
1385
+ node_count_changed = pyqtSignal(int)
1386
+ connection_count_changed = pyqtSignal(int)
1387
+ status_message = pyqtSignal(str)
1388
+ selection_changed = pyqtSignal(list)
1389
+
1390
+ def __init__(self, parent=None):
1391
+ super().__init__(parent)
1392
+
1393
+ self.scene = QGraphicsScene(self)
1394
+ self.scene.setSceneRect(-10000, -10000, 20000, 20000)
1395
+ self.setScene(self.scene)
1396
+
1397
+ self.setRenderHint(QPainter.RenderHint.Antialiasing)
1398
+ self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
1399
+ self.setRenderHint(QPainter.RenderHint.TextAntialiasing)
1400
+ self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
1401
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
1402
+ self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
1403
+ self.setDragMode(QGraphicsView.DragMode.NoDrag)
1404
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
1405
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
1406
+
1407
+ self._panning = False
1408
+ self._pan_start = QPointF()
1409
+ self._zoom_level = 1.0
1410
+ self._connecting_mode = False
1411
+ self._connect_start_node: Optional[VisualNode] = None
1412
+ self._temp_line: Optional[QGraphicsLineItem] = None
1413
+ self._rubber_band: Optional[QRubberBand] = None
1414
+ self._rubber_band_origin = QPointF()
1415
+ self._grid_visible = True
1416
+ self._clipboard: List[NodeData] = []
1417
+
1418
+ self.nodes: Dict[str, VisualNode] = {}
1419
+ self.connections: List[ConnectionLine] = []
1420
+ self.groups: Dict[str, GroupData] = {}
1421
+
1422
+ self.undo_stack = QUndoStack(self)
1423
+
1424
+ self._draw_background()
1425
+ self._apply_style()
1426
+ self._setup_shortcuts()
1427
+
1428
+ def _apply_style(self):
1429
+ self.setStyleSheet(f'''
1430
+ QGraphicsView {{
1431
+ border: none;
1432
+ background-color: {THEME['bg_dark']};
1433
+ }}
1434
+ ''')
1435
+
1436
+ def _setup_shortcuts(self):
1437
+ """Setup keyboard shortcuts."""
1438
+ QShortcut(QKeySequence.StandardKey.Copy, self, self.copy_selected)
1439
+ QShortcut(QKeySequence.StandardKey.Paste, self, self.paste_nodes)
1440
+ QShortcut(QKeySequence.StandardKey.Cut, self, self.cut_selected)
1441
+ QShortcut(QKeySequence("Ctrl+D"), self, self.duplicate_selected)
1442
+ QShortcut(QKeySequence.StandardKey.Undo, self, self.undo_stack.undo)
1443
+ QShortcut(QKeySequence.StandardKey.Redo, self, self.undo_stack.redo)
1444
+ QShortcut(QKeySequence.StandardKey.SelectAll, self, self.select_all)
1445
+ QShortcut(QKeySequence("Ctrl+G"), self, self.group_selected)
1446
+
1447
+ def _draw_background(self):
1448
+ """Draw professional grid background"""
1449
+ minor_pen = QPen(QColor(THEME['bg_secondary']), 0.5)
1450
+ minor_size = 25
1451
+ for x in range(-10000, 10000, minor_size):
1452
+ line = self.scene.addLine(x, -10000, x, 10000, minor_pen)
1453
+ line.setZValue(-100)
1454
+ for y in range(-10000, 10000, minor_size):
1455
+ line = self.scene.addLine(-10000, y, 10000, y, minor_pen)
1456
+ line.setZValue(-100)
1457
+
1458
+ major_pen = QPen(QColor(THEME['bg_tertiary']), 1)
1459
+ major_size = 100
1460
+ for x in range(-10000, 10000, major_size):
1461
+ line = self.scene.addLine(x, -10000, x, 10000, major_pen)
1462
+ line.setZValue(-99)
1463
+ for y in range(-10000, 10000, major_size):
1464
+ line = self.scene.addLine(-10000, y, 10000, y, major_pen)
1465
+ line.setZValue(-99)
1466
+
1467
+ center_pen = QPen(QColor(THEME['accent_blue']).darker(200), 2)
1468
+ self.scene.addLine(-50, 0, 50, 0, center_pen).setZValue(-98)
1469
+ self.scene.addLine(0, -50, 0, 50, center_pen).setZValue(-98)
1470
+
1471
+ def add_node(self, node_type: str, name: str, x: float = None, y: float = None) -> VisualNode:
1472
+ """Add a new node to the canvas (with undo support)"""
1473
+ if x is None:
1474
+ x = self.mapToScene(self.viewport().rect().center()).x() - 100
1475
+ if y is None:
1476
+ y = self.mapToScene(self.viewport().rect().center()).y() - 50
1477
+
1478
+ node_data = NodeData(
1479
+ id=str(uuid.uuid4())[:8],
1480
+ node_type=node_type,
1481
+ name=name,
1482
+ x=x,
1483
+ y=y
1484
+ )
1485
+
1486
+ cmd = AddNodeCommand(self, node_data, f"Add {name}")
1487
+ self.undo_stack.push(cmd)
1488
+
1489
+ return self.nodes.get(node_data.id)
1490
+
1491
+ def _add_node_internal(self, node_data: NodeData) -> VisualNode:
1492
+ """Internal method to add node without undo."""
1493
+ visual_node = VisualNode(node_data)
1494
+ self.scene.addItem(visual_node)
1495
+ self.nodes[node_data.id] = visual_node
1496
+ self.node_count_changed.emit(len(self.nodes))
1497
+ self.status_message.emit(f"Created {node_data.node_type}: {node_data.name}")
1498
+ return visual_node
1499
+
1500
+ def connect_nodes(self, start: VisualNode, end: VisualNode,
1501
+ label: str = "", style: str = "solid") -> Optional[ConnectionLine]:
1502
+ """Create a connection between nodes"""
1503
+ if start == end:
1504
+ return None
1505
+
1506
+ for conn in self.connections:
1507
+ if (conn.start_node == start and conn.end_node == end) or \
1508
+ (conn.start_node == end and conn.end_node == start):
1509
+ self.status_message.emit("Connection already exists")
1510
+ return None
1511
+
1512
+ conn_data = ConnectionData(
1513
+ id=str(uuid.uuid4())[:8],
1514
+ start_node_id=start.node_data.id,
1515
+ end_node_id=end.node_data.id,
1516
+ label=label,
1517
+ style=style
1518
+ )
1519
+
1520
+ cmd = AddConnectionCommand(self, conn_data)
1521
+ self.undo_stack.push(cmd)
1522
+
1523
+ return self.connections[-1] if self.connections else None
1524
+
1525
+ def _add_connection_internal(self, conn_data: ConnectionData) -> Optional[ConnectionLine]:
1526
+ """Internal method to add connection without undo."""
1527
+ if conn_data.start_node_id not in self.nodes or conn_data.end_node_id not in self.nodes:
1528
+ return None
1529
+
1530
+ start = self.nodes[conn_data.start_node_id]
1531
+ end = self.nodes[conn_data.end_node_id]
1532
+
1533
+ connection = ConnectionLine(start, end, conn_data)
1534
+ self.scene.addItem(connection)
1535
+ self.connections.append(connection)
1536
+
1537
+ if end.node_data.id not in start.node_data.connections:
1538
+ start.node_data.connections.append(end.node_data.id)
1539
+ if start.node_data.id not in end.node_data.connections:
1540
+ end.node_data.connections.append(start.node_data.id)
1541
+
1542
+ self.connection_count_changed.emit(len(self.connections))
1543
+ return connection
1544
+
1545
+ def delete_node(self, node: VisualNode):
1546
+ """Delete a node and its connections"""
1547
+ cmd = DeleteNodeCommand(self, node, f"Delete {node.node_data.name}")
1548
+ self.undo_stack.push(cmd)
1549
+
1550
+ def _delete_node_internal(self, node: VisualNode):
1551
+ """Internal method to delete node without undo."""
1552
+ for conn in list(node.connections):
1553
+ self._delete_connection_internal(conn)
1554
+
1555
+ if node.node_data.id in self.nodes:
1556
+ del self.nodes[node.node_data.id]
1557
+
1558
+ self.scene.removeItem(node)
1559
+ self.node_count_changed.emit(len(self.nodes))
1560
+ self.status_message.emit(f"Deleted: {node.node_data.name}")
1561
+
1562
+ def delete_connection(self, conn: ConnectionLine):
1563
+ """Delete a connection"""
1564
+ self._delete_connection_internal(conn)
1565
+
1566
+ def _delete_connection_internal(self, conn: ConnectionLine):
1567
+ """Internal method to delete connection."""
1568
+ if conn in self.connections:
1569
+ self.connections.remove(conn)
1570
+ conn.remove()
1571
+ self.scene.removeItem(conn)
1572
+ self.connection_count_changed.emit(len(self.connections))
1573
+
1574
+ def copy_selected(self):
1575
+ """Copy selected nodes to clipboard."""
1576
+ selected = [item for item in self.scene.selectedItems() if isinstance(item, VisualNode)]
1577
+ if not selected:
1578
+ return
1579
+
1580
+ self._clipboard = [copy.deepcopy(node.node_data) for node in selected]
1581
+ self.status_message.emit(f"Copied {len(self._clipboard)} node(s)")
1582
+
1583
+ def paste_nodes(self):
1584
+ """Paste nodes from clipboard."""
1585
+ if not self._clipboard:
1586
+ return
1587
+
1588
+ center = self.mapToScene(self.viewport().rect().center())
1589
+ offset = 50
1590
+
1591
+ self.scene.clearSelection()
1592
+
1593
+ for node_data in self._clipboard:
1594
+ new_data = copy.deepcopy(node_data)
1595
+ new_data.id = str(uuid.uuid4())[:8]
1596
+ new_data.x = center.x() + offset
1597
+ new_data.y = center.y() + offset
1598
+ new_data.connections = []
1599
+
1600
+ visual_node = self._add_node_internal(new_data)
1601
+ if visual_node:
1602
+ visual_node.setSelected(True)
1603
+ offset += 30
1604
+
1605
+ self.status_message.emit(f"Pasted {len(self._clipboard)} node(s)")
1606
+
1607
+ def cut_selected(self):
1608
+ """Cut selected nodes."""
1609
+ self.copy_selected()
1610
+ for item in self.scene.selectedItems():
1611
+ if isinstance(item, VisualNode):
1612
+ self.delete_node(item)
1613
+
1614
+ def duplicate_selected(self):
1615
+ """Duplicate selected nodes."""
1616
+ self.copy_selected()
1617
+ self.paste_nodes()
1618
+
1619
+ def select_all(self):
1620
+ """Select all nodes."""
1621
+ for node in self.nodes.values():
1622
+ node.setSelected(True)
1623
+
1624
+ def group_selected(self):
1625
+ """Group selected nodes."""
1626
+ selected = [item for item in self.scene.selectedItems() if isinstance(item, VisualNode)]
1627
+ if len(selected) < 2:
1628
+ self.status_message.emit("Select at least 2 nodes to group")
1629
+ return
1630
+
1631
+ name, ok = QInputDialog.getText(self, "Group Name", "Enter group name:")
1632
+ if not ok or not name:
1633
+ return
1634
+
1635
+ group_data = GroupData(
1636
+ name=name,
1637
+ node_ids=[node.node_data.id for node in selected]
1638
+ )
1639
+
1640
+ for node in selected:
1641
+ node.node_data.group_id = group_data.id
1642
+
1643
+ self.groups[group_data.id] = group_data
1644
+ self.status_message.emit(f"Created group: {name}")
1645
+
1646
+ def search_nodes(self, query: str):
1647
+ """Search and highlight nodes matching query."""
1648
+ query = query.lower()
1649
+ for node in self.nodes.values():
1650
+ matches = query in node.node_data.name.lower() or \
1651
+ query in node.node_data.description.lower()
1652
+ node.setOpacity(1.0 if matches or not query else 0.3)
1653
+
1654
+ def filter_by_type(self, type_filter: str):
1655
+ """Filter nodes by type."""
1656
+ if type_filter == "all":
1657
+ for node in self.nodes.values():
1658
+ node.setVisible(True)
1659
+ elif type_filter.startswith("cat:"):
1660
+ category = type_filter[4:]
1661
+ types = NODE_CATEGORIES.get(category, [])
1662
+ for node in self.nodes.values():
1663
+ node.setVisible(node.node_data.node_type in types)
1664
+ else:
1665
+ for node in self.nodes.values():
1666
+ node.setVisible(node.node_data.node_type == type_filter)
1667
+
1668
+ def get_map_data(self) -> MapData:
1669
+ """Export current state to MapData"""
1670
+ transform = self.transform()
1671
+ center = self.mapToScene(self.viewport().rect().center())
1672
+
1673
+ return MapData(
1674
+ name="Untitled",
1675
+ nodes=[n.node_data for n in self.nodes.values()],
1676
+ connections=[c.connection_data for c in self.connections],
1677
+ groups=list(self.groups.values()),
1678
+ viewport_x=center.x(),
1679
+ viewport_y=center.y(),
1680
+ viewport_zoom=self._zoom_level,
1681
+ grid_visible=self._grid_visible
1682
+ )
1683
+
1684
+ def load_map_data(self, map_data: MapData):
1685
+ """Load map from MapData"""
1686
+ self.clear_all()
1687
+
1688
+ for node_data in map_data.nodes:
1689
+ visual_node = VisualNode(node_data)
1690
+ self.scene.addItem(visual_node)
1691
+ self.nodes[node_data.id] = visual_node
1692
+
1693
+ for conn_data in map_data.connections:
1694
+ if conn_data.start_node_id in self.nodes and conn_data.end_node_id in self.nodes:
1695
+ start = self.nodes[conn_data.start_node_id]
1696
+ end = self.nodes[conn_data.end_node_id]
1697
+ connection = ConnectionLine(start, end, conn_data)
1698
+ self.scene.addItem(connection)
1699
+ self.connections.append(connection)
1700
+
1701
+ for group_data in map_data.groups:
1702
+ self.groups[group_data.id] = group_data
1703
+
1704
+ self._zoom_level = map_data.viewport_zoom
1705
+ self.resetTransform()
1706
+ self.scale(self._zoom_level, self._zoom_level)
1707
+ self.centerOn(map_data.viewport_x, map_data.viewport_y)
1708
+
1709
+ self.node_count_changed.emit(len(self.nodes))
1710
+ self.connection_count_changed.emit(len(self.connections))
1711
+
1712
+ def clear_all(self):
1713
+ """Clear canvas"""
1714
+ for conn in list(self.connections):
1715
+ conn.remove()
1716
+ self.scene.removeItem(conn)
1717
+ for node in list(self.nodes.values()):
1718
+ self.scene.removeItem(node)
1719
+
1720
+ self.nodes.clear()
1721
+ self.connections.clear()
1722
+ self.groups.clear()
1723
+ self.undo_stack.clear()
1724
+
1725
+ self.node_count_changed.emit(0)
1726
+ self.connection_count_changed.emit(0)
1727
+
1728
+ def export_to_image(self, file_path: str, format: str = "PNG"):
1729
+ """Export canvas to image."""
1730
+ if not self.nodes:
1731
+ return
1732
+
1733
+ min_x = min(n.node_data.x for n in self.nodes.values()) - 50
1734
+ max_x = max(n.node_data.x + n.node_data.width for n in self.nodes.values()) + 50
1735
+ min_y = min(n.node_data.y for n in self.nodes.values()) - 50
1736
+ max_y = max(n.node_data.y + n.node_data.height for n in self.nodes.values()) + 50
1737
+
1738
+ rect = QRectF(min_x, min_y, max_x - min_x, max_y - min_y)
1739
+
1740
+ if format.upper() == "SVG":
1741
+ from PyQt6.QtSvg import QSvgGenerator
1742
+ generator = QSvgGenerator()
1743
+ generator.setFileName(file_path)
1744
+ generator.setSize(QSize(int(rect.width()), int(rect.height())))
1745
+ generator.setViewBox(QRect(0, 0, int(rect.width()), int(rect.height())))
1746
+
1747
+ painter = QPainter(generator)
1748
+ self.scene.render(painter, QRectF(0, 0, rect.width(), rect.height()), rect)
1749
+ painter.end()
1750
+ else:
1751
+ image = QImage(int(rect.width()), int(rect.height()), QImage.Format.Format_ARGB32)
1752
+ image.fill(QColor(THEME['bg_dark']))
1753
+
1754
+ painter = QPainter(image)
1755
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1756
+ self.scene.render(painter, QRectF(0, 0, rect.width(), rect.height()), rect)
1757
+ painter.end()
1758
+
1759
+ image.save(file_path, format)
1760
+
1761
+ self.status_message.emit(f"Exported to {file_path}")
1762
+
1763
+ def start_connection_mode(self, start_node: VisualNode):
1764
+ """Start connection mode from a node"""
1765
+ self._connecting_mode = True
1766
+ self._connect_start_node = start_node
1767
+ self.setCursor(Qt.CursorShape.CrossCursor)
1768
+ self.status_message.emit("Click on another node to connect (ESC to cancel)")
1769
+
1770
+ def cancel_connection_mode(self):
1771
+ """Cancel connection mode"""
1772
+ self._connecting_mode = False
1773
+ self._connect_start_node = None
1774
+ if self._temp_line:
1775
+ self.scene.removeItem(self._temp_line)
1776
+ self._temp_line = None
1777
+ self.setCursor(Qt.CursorShape.ArrowCursor)
1778
+ self.status_message.emit("Connection cancelled")
1779
+
1780
+ def wheelEvent(self, event: QWheelEvent):
1781
+ """Smooth zoom with wheel"""
1782
+ factor = 1.1
1783
+
1784
+ if event.angleDelta().y() > 0:
1785
+ if self._zoom_level < 3.0:
1786
+ self._zoom_level *= factor
1787
+ self.scale(factor, factor)
1788
+ else:
1789
+ if self._zoom_level > 0.2:
1790
+ self._zoom_level /= factor
1791
+ self.scale(1/factor, 1/factor)
1792
+
1793
+ def mousePressEvent(self, event: QMouseEvent):
1794
+ if event.button() == Qt.MouseButton.MiddleButton:
1795
+ self._panning = True
1796
+ self._pan_start = event.position()
1797
+ self.setCursor(Qt.CursorShape.ClosedHandCursor)
1798
+ elif self._connecting_mode and event.button() == Qt.MouseButton.LeftButton:
1799
+ scene_pos = self.mapToScene(event.pos())
1800
+ item = self.scene.itemAt(scene_pos, self.transform())
1801
+
1802
+ target_node = self._find_node_at(item)
1803
+ if target_node and target_node != self._connect_start_node:
1804
+ self.connect_nodes(self._connect_start_node, target_node)
1805
+ self.cancel_connection_mode()
1806
+ elif not target_node:
1807
+ self.cancel_connection_mode()
1808
+ elif event.button() == Qt.MouseButton.LeftButton:
1809
+ scene_pos = self.mapToScene(event.pos())
1810
+ item = self.scene.itemAt(scene_pos, self.transform())
1811
+ if not item or not self._find_node_at(item):
1812
+ self._rubber_band_origin = event.pos()
1813
+ if not self._rubber_band:
1814
+ self._rubber_band = QRubberBand(QRubberBand.Shape.Rectangle, self)
1815
+ self._rubber_band.setGeometry(QRect(event.pos(), QSize()))
1816
+ self._rubber_band.show()
1817
+ super().mousePressEvent(event)
1818
+ else:
1819
+ super().mousePressEvent(event)
1820
+
1821
+ def mouseMoveEvent(self, event: QMouseEvent):
1822
+ if self._panning:
1823
+ delta = event.position() - self._pan_start
1824
+ self._pan_start = event.position()
1825
+ self.horizontalScrollBar().setValue(
1826
+ int(self.horizontalScrollBar().value() - delta.x())
1827
+ )
1828
+ self.verticalScrollBar().setValue(
1829
+ int(self.verticalScrollBar().value() - delta.y())
1830
+ )
1831
+ elif self._rubber_band and self._rubber_band.isVisible():
1832
+ self._rubber_band.setGeometry(
1833
+ QRect(self._rubber_band_origin, event.pos()).normalized()
1834
+ )
1835
+ elif self._connecting_mode and self._connect_start_node:
1836
+ scene_pos = self.mapToScene(event.pos())
1837
+ start = self._connect_start_node.get_connection_point('right')
1838
+
1839
+ if not self._temp_line:
1840
+ self._temp_line = self.scene.addLine(
1841
+ start.x(), start.y(), scene_pos.x(), scene_pos.y(),
1842
+ QPen(QColor(THEME['accent_blue']), 2, Qt.PenStyle.DashLine)
1843
+ )
1844
+ else:
1845
+ self._temp_line.setLine(start.x(), start.y(), scene_pos.x(), scene_pos.y())
1846
+ else:
1847
+ super().mouseMoveEvent(event)
1848
+
1849
+ def mouseReleaseEvent(self, event: QMouseEvent):
1850
+ if event.button() == Qt.MouseButton.MiddleButton:
1851
+ self._panning = False
1852
+ self.setCursor(Qt.CursorShape.ArrowCursor)
1853
+ elif self._rubber_band and self._rubber_band.isVisible():
1854
+ selection_rect = self.mapToScene(self._rubber_band.geometry()).boundingRect()
1855
+ self._rubber_band.hide()
1856
+
1857
+ if not (event.modifiers() & Qt.KeyboardModifier.ShiftModifier):
1858
+ self.scene.clearSelection()
1859
+
1860
+ for node in self.nodes.values():
1861
+ if selection_rect.intersects(node.sceneBoundingRect()):
1862
+ node.setSelected(True)
1863
+ else:
1864
+ super().mouseReleaseEvent(event)
1865
+
1866
+ def keyPressEvent(self, event: QKeyEvent):
1867
+ if event.key() == Qt.Key.Key_Escape:
1868
+ if self._connecting_mode:
1869
+ self.cancel_connection_mode()
1870
+ else:
1871
+ self.scene.clearSelection()
1872
+ elif event.key() == Qt.Key.Key_Delete or event.key() == Qt.Key.Key_Backspace:
1873
+ for item in self.scene.selectedItems():
1874
+ if isinstance(item, VisualNode):
1875
+ self.delete_node(item)
1876
+ elif event.key() == Qt.Key.Key_Home:
1877
+ self.centerOn(0, 0)
1878
+ elif event.key() == Qt.Key.Key_0 and event.modifiers() & Qt.KeyboardModifier.ControlModifier:
1879
+ self._zoom_level = 1.0
1880
+ self.resetTransform()
1881
+ else:
1882
+ super().keyPressEvent(event)
1883
+
1884
+ def _find_node_at(self, item) -> Optional[VisualNode]:
1885
+ """Find the VisualNode for an item"""
1886
+ if isinstance(item, VisualNode):
1887
+ return item
1888
+ elif item:
1889
+ parent = item.parentItem()
1890
+ while parent:
1891
+ if isinstance(parent, VisualNode):
1892
+ return parent
1893
+ parent = parent.parentItem()
1894
+ return None
1895
+
1896
+ def _create_categorized_menu(self, parent_menu: QMenu, scene_pos: QPointF):
1897
+ """Create categorized node creation submenu."""
1898
+ for category, types in NODE_CATEGORIES.items():
1899
+ cat_menu = parent_menu.addMenu(category)
1900
+ for node_type in types:
1901
+ if node_type in NODE_TYPES:
1902
+ config = NODE_TYPES[node_type]
1903
+ action = cat_menu.addAction(f"{config['icon']} {config['label']}")
1904
+ action.setData((node_type, scene_pos))
1905
+
1906
+ def contextMenuEvent(self, event):
1907
+ """Professional context menu"""
1908
+ scene_pos = self.mapToScene(event.pos())
1909
+ item = self.scene.itemAt(scene_pos, self.transform())
1910
+ node = self._find_node_at(item)
1911
+
1912
+ menu = QMenu(self)
1913
+ menu.setStyleSheet(f'''
1914
+ QMenu {{
1915
+ background-color: {THEME['bg_secondary']};
1916
+ border: 1px solid {THEME['border']};
1917
+ border-radius: 8px;
1918
+ padding: 8px;
1919
+ }}
1920
+ QMenu::item {{
1921
+ background-color: transparent;
1922
+ padding: 10px 24px 10px 16px;
1923
+ color: {THEME['text_primary']};
1924
+ border-radius: 4px;
1925
+ margin: 2px 4px;
1926
+ }}
1927
+ QMenu::item:selected {{
1928
+ background-color: {THEME['accent_blue']};
1929
+ }}
1930
+ QMenu::separator {{
1931
+ height: 1px;
1932
+ background: {THEME['border']};
1933
+ margin: 6px 8px;
1934
+ }}
1935
+ ''')
1936
+
1937
+ if node:
1938
+ props_action = menu.addAction("Properties...")
1939
+ rename_action = menu.addAction("Rename")
1940
+ menu.addSeparator()
1941
+
1942
+ conn_menu = menu.addMenu("Connections")
1943
+ connect_action = conn_menu.addAction("Connect to...")
1944
+ disconnect_all = conn_menu.addAction("Disconnect All")
1945
+
1946
+ menu.addSeparator()
1947
+
1948
+ edit_menu = menu.addMenu("Edit")
1949
+ copy_action = edit_menu.addAction("Copy (Ctrl+C)")
1950
+ cut_action = edit_menu.addAction("Cut (Ctrl+X)")
1951
+ duplicate_action = edit_menu.addAction("Duplicate (Ctrl+D)")
1952
+
1953
+ menu.addSeparator()
1954
+
1955
+ code_menu = menu.addMenu("Code")
1956
+ preview_code = code_menu.addAction("Preview Code...")
1957
+ gen_cpp_h = code_menu.addAction("Generate C++ Header")
1958
+ gen_cpp_cpp = code_menu.addAction("Generate C++ Source")
1959
+ gen_python = code_menu.addAction("Generate Python")
1960
+
1961
+ menu.addSeparator()
1962
+ delete_action = menu.addAction("Delete")
1963
+
1964
+ action = menu.exec(event.globalPos())
1965
+
1966
+ if action == props_action:
1967
+ dialog = NodePropertiesDialog(node, self)
1968
+ if dialog.exec() == QDialog.DialogCode.Accepted:
1969
+ name, desc, ntype = dialog.get_values()
1970
+ if name:
1971
+ node.update_name(name)
1972
+ node.update_description(desc)
1973
+
1974
+ elif action == rename_action:
1975
+ name, ok = QInputDialog.getText(
1976
+ self, "Rename", "New name:", text=node.node_data.name
1977
+ )
1978
+ if ok and name:
1979
+ node.update_name(name)
1980
+
1981
+ elif action == connect_action:
1982
+ self.start_connection_mode(node)
1983
+
1984
+ elif action == disconnect_all:
1985
+ for conn in list(node.connections):
1986
+ self.delete_connection(conn)
1987
+
1988
+ elif action == copy_action:
1989
+ node.setSelected(True)
1990
+ self.copy_selected()
1991
+
1992
+ elif action == cut_action:
1993
+ node.setSelected(True)
1994
+ self.cut_selected()
1995
+
1996
+ elif action == duplicate_action:
1997
+ node.setSelected(True)
1998
+ self.duplicate_selected()
1999
+
2000
+ elif action == preview_code:
2001
+ dialog = CodePreviewDialog(node, self)
2002
+ dialog.exec()
2003
+
2004
+ elif action == gen_cpp_h:
2005
+ code = CodeGenerator.generate_cpp_header(node.node_data)
2006
+ QApplication.clipboard().setText(code)
2007
+ self.status_message.emit("C++ header copied to clipboard")
2008
+
2009
+ elif action == gen_cpp_cpp:
2010
+ code = CodeGenerator.generate_cpp_source(node.node_data)
2011
+ QApplication.clipboard().setText(code)
2012
+ self.status_message.emit("C++ source copied to clipboard")
2013
+
2014
+ elif action == gen_python:
2015
+ code = CodeGenerator.generate_python(node.node_data)
2016
+ QApplication.clipboard().setText(code)
2017
+ self.status_message.emit("Python code copied to clipboard")
2018
+
2019
+ elif action == delete_action:
2020
+ self.delete_node(node)
2021
+
2022
+ else:
2023
+ create_menu = menu.addMenu("New")
2024
+ self._create_categorized_menu(create_menu, scene_pos)
2025
+
2026
+ if self._clipboard:
2027
+ menu.addSeparator()
2028
+ paste_action = menu.addAction("Paste (Ctrl+V)")
2029
+
2030
+ menu.addSeparator()
2031
+
2032
+ view_menu = menu.addMenu("View")
2033
+ center_action = view_menu.addAction("Center View (Home)")
2034
+ fit_action = view_menu.addAction("Fit All Nodes")
2035
+ reset_zoom_action = view_menu.addAction("Reset Zoom (Ctrl+0)")
2036
+ view_menu.addSeparator()
2037
+ toggle_grid = view_menu.addAction("Toggle Grid")
2038
+ toggle_grid.setCheckable(True)
2039
+ toggle_grid.setChecked(self._grid_visible)
2040
+
2041
+ menu.addSeparator()
2042
+
2043
+ select_menu = menu.addMenu("Selection")
2044
+ select_all_action = select_menu.addAction("Select All (Ctrl+A)")
2045
+ deselect_action = select_menu.addAction("Deselect All")
2046
+
2047
+ menu.addSeparator()
2048
+
2049
+ export_menu = menu.addMenu("Export")
2050
+ export_png = export_menu.addAction("Export as PNG...")
2051
+ export_svg = export_menu.addAction("Export as SVG...")
2052
+
2053
+ action = menu.exec(event.globalPos())
2054
+
2055
+ if action and hasattr(action, 'data') and action.data():
2056
+ data = action.data()
2057
+ if isinstance(data, tuple):
2058
+ node_type, pos = data
2059
+ name, ok = QInputDialog.getText(
2060
+ self, f"New {NODE_TYPES[node_type]['label']}", "Name:"
2061
+ )
2062
+ if ok and name:
2063
+ self.add_node(node_type, name, pos.x(), pos.y())
2064
+
2065
+ elif action and action.text() == "Paste (Ctrl+V)":
2066
+ self.paste_nodes()
2067
+
2068
+ elif action == center_action:
2069
+ self.centerOn(0, 0)
2070
+
2071
+ elif action == fit_action:
2072
+ if self.nodes:
2073
+ min_x = min(n.node_data.x for n in self.nodes.values())
2074
+ max_x = max(n.node_data.x + n.node_data.width for n in self.nodes.values())
2075
+ min_y = min(n.node_data.y for n in self.nodes.values())
2076
+ max_y = max(n.node_data.y + n.node_data.height for n in self.nodes.values())
2077
+ rect = QRectF(min_x - 50, min_y - 50, max_x - min_x + 100, max_y - min_y + 100)
2078
+ self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
2079
+
2080
+ elif action == reset_zoom_action:
2081
+ self._zoom_level = 1.0
2082
+ self.resetTransform()
2083
+
2084
+ elif action == toggle_grid:
2085
+ self._grid_visible = not self._grid_visible
2086
+
2087
+ elif action == select_all_action:
2088
+ self.select_all()
2089
+
2090
+ elif action == deselect_action:
2091
+ self.scene.clearSelection()
2092
+
2093
+ elif action == export_png:
2094
+ file_path, _ = QFileDialog.getSaveFileName(
2095
+ self, "Export as PNG", "", "PNG Files (*.png)"
2096
+ )
2097
+ if file_path:
2098
+ self.export_to_image(file_path, "PNG")
2099
+
2100
+ elif action == export_svg:
2101
+ file_path, _ = QFileDialog.getSaveFileName(
2102
+ self, "Export as SVG", "", "SVG Files (*.svg)"
2103
+ )
2104
+ if file_path:
2105
+ self.export_to_image(file_path, "SVG")
2106
+
2107
+
2108
+ # ========================================================================
2109
+ # File Tree Panel
2110
+ # ========================================================================
2111
+
2112
+ class FileTreePanel(QFrame):
2113
+ """Professional file tree for .ma map files"""
2114
+
2115
+ file_selected = pyqtSignal(str)
2116
+ file_created = pyqtSignal(str)
2117
+ file_deleted = pyqtSignal(str)
2118
+
2119
+ def __init__(self, project_path: Path, parent=None):
2120
+ super().__init__(parent)
2121
+ self.project_path = project_path
2122
+ self.maps_dir = project_path / ".includecpp" / "maps"
2123
+ self.maps_dir.mkdir(parents=True, exist_ok=True)
2124
+
2125
+ self._setup_ui()
2126
+ self._load_files()
2127
+
2128
+ def _setup_ui(self):
2129
+ self.setStyleSheet(f'''
2130
+ QFrame {{
2131
+ background-color: {THEME['bg_primary']};
2132
+ border-right: 1px solid {THEME['border']};
2133
+ }}
2134
+ ''')
2135
+
2136
+ layout = QVBoxLayout(self)
2137
+ layout.setContentsMargins(0, 0, 0, 0)
2138
+ layout.setSpacing(0)
2139
+
2140
+ header = QFrame()
2141
+ header.setFixedHeight(48)
2142
+ header.setStyleSheet(f'''
2143
+ QFrame {{
2144
+ background-color: {THEME['bg_secondary']};
2145
+ border-bottom: 1px solid {THEME['border']};
2146
+ }}
2147
+ ''')
2148
+
2149
+ header_layout = QHBoxLayout(header)
2150
+ header_layout.setContentsMargins(16, 0, 12, 0)
2151
+
2152
+ title = QLabel("Maps")
2153
+ title.setFont(QFont(SYSTEM_FONT, 12, QFont.Weight.Bold))
2154
+ title.setStyleSheet(f'color: {THEME["text_primary"]};')
2155
+ header_layout.addWidget(title)
2156
+
2157
+ header_layout.addStretch()
2158
+
2159
+ new_btn = QPushButton("+")
2160
+ new_btn.setFixedSize(28, 28)
2161
+ new_btn.setStyleSheet(f'''
2162
+ QPushButton {{
2163
+ background-color: {THEME['accent_blue']};
2164
+ border: none;
2165
+ border-radius: 14px;
2166
+ color: white;
2167
+ font-size: 18px;
2168
+ font-weight: bold;
2169
+ }}
2170
+ QPushButton:hover {{
2171
+ background-color: {QColor(THEME['accent_blue']).lighter(115).name()};
2172
+ }}
2173
+ ''')
2174
+ new_btn.setCursor(Qt.CursorShape.PointingHandCursor)
2175
+ new_btn.setToolTip("Create new map")
2176
+ new_btn.clicked.connect(self._create_new)
2177
+ header_layout.addWidget(new_btn)
2178
+
2179
+ layout.addWidget(header)
2180
+
2181
+ self.tree = QTreeWidget()
2182
+ self.tree.setHeaderHidden(True)
2183
+ self.tree.setIndentation(0)
2184
+ self.tree.setAnimated(True)
2185
+ self.tree.setStyleSheet(f'''
2186
+ QTreeWidget {{
2187
+ background-color: {THEME['bg_primary']};
2188
+ border: none;
2189
+ color: {THEME['text_primary']};
2190
+ font-size: 12px;
2191
+ outline: none;
2192
+ }}
2193
+ QTreeWidget::item {{
2194
+ padding: 12px 16px;
2195
+ border-bottom: 1px solid {THEME['bg_secondary']};
2196
+ }}
2197
+ QTreeWidget::item:selected {{
2198
+ background-color: {THEME['accent_blue']}40;
2199
+ border-left: 3px solid {THEME['accent_blue']};
2200
+ }}
2201
+ QTreeWidget::item:hover:!selected {{
2202
+ background-color: {THEME['bg_tertiary']};
2203
+ }}
2204
+ ''')
2205
+ self.tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
2206
+ self.tree.customContextMenuRequested.connect(self._context_menu)
2207
+ self.tree.itemDoubleClicked.connect(self._on_double_click)
2208
+ layout.addWidget(self.tree)
2209
+
2210
+ def _load_files(self):
2211
+ """Load .ma files"""
2212
+ self.tree.clear()
2213
+
2214
+ for file_path in sorted(self.maps_dir.glob("*.ma")):
2215
+ item = QTreeWidgetItem([f" {file_path.stem}"])
2216
+ item.setData(0, Qt.ItemDataRole.UserRole, str(file_path))
2217
+ item.setToolTip(0, file_path.name)
2218
+ self.tree.addTopLevelItem(item)
2219
+
2220
+ def _create_new(self):
2221
+ """Create new map file"""
2222
+ name, ok = QInputDialog.getText(self, "New Map", "Map name:")
2223
+ if ok and name:
2224
+ safe_name = name.replace(" ", "_").replace("/", "_")
2225
+ file_path = self.maps_dir / f"{safe_name}.ma"
2226
+
2227
+ map_data = MapData(name=name)
2228
+ file_path.write_text(json.dumps(map_data.to_dict(), indent=2), encoding='utf-8')
2229
+
2230
+ self._load_files()
2231
+ self.file_created.emit(str(file_path))
2232
+
2233
+ def _on_double_click(self, item, column):
2234
+ file_path = item.data(0, Qt.ItemDataRole.UserRole)
2235
+ if file_path:
2236
+ self.file_selected.emit(file_path)
2237
+
2238
+ def _context_menu(self, pos):
2239
+ item = self.tree.itemAt(pos)
2240
+ if not item:
2241
+ return
2242
+
2243
+ file_path = item.data(0, Qt.ItemDataRole.UserRole)
2244
+
2245
+ menu = QMenu(self)
2246
+ menu.setStyleSheet(f'''
2247
+ QMenu {{
2248
+ background-color: {THEME['bg_secondary']};
2249
+ border: 1px solid {THEME['border']};
2250
+ border-radius: 8px;
2251
+ padding: 4px;
2252
+ }}
2253
+ QMenu::item {{
2254
+ padding: 10px 20px;
2255
+ color: {THEME['text_primary']};
2256
+ border-radius: 4px;
2257
+ margin: 2px;
2258
+ }}
2259
+ QMenu::item:selected {{
2260
+ background-color: {THEME['accent_blue']};
2261
+ }}
2262
+ ''')
2263
+
2264
+ open_action = menu.addAction("Open")
2265
+ menu.addSeparator()
2266
+ rename_action = menu.addAction("Rename")
2267
+ clear_action = menu.addAction("Clear Contents")
2268
+ menu.addSeparator()
2269
+ delete_action = menu.addAction("Delete")
2270
+
2271
+ action = menu.exec(self.tree.mapToGlobal(pos))
2272
+
2273
+ if action == open_action:
2274
+ self.file_selected.emit(file_path)
2275
+
2276
+ elif action == rename_action:
2277
+ old_name = Path(file_path).stem
2278
+ new_name, ok = QInputDialog.getText(
2279
+ self, "Rename", "New name:", text=old_name
2280
+ )
2281
+ if ok and new_name and new_name != old_name:
2282
+ new_path = self.maps_dir / f"{new_name}.ma"
2283
+ Path(file_path).rename(new_path)
2284
+ self._load_files()
2285
+
2286
+ elif action == clear_action:
2287
+ map_data = MapData(name=Path(file_path).stem)
2288
+ Path(file_path).write_text(json.dumps(map_data.to_dict(), indent=2), encoding='utf-8')
2289
+ self.file_selected.emit(file_path)
2290
+
2291
+ elif action == delete_action:
2292
+ reply = QMessageBox.question(
2293
+ self, "Delete Map",
2294
+ f"Are you sure you want to delete '{Path(file_path).stem}'?",
2295
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
2296
+ )
2297
+ if reply == QMessageBox.StandardButton.Yes:
2298
+ Path(file_path).unlink()
2299
+ self._load_files()
2300
+ self.file_deleted.emit(file_path)
2301
+
2302
+
2303
+ # ========================================================================
2304
+ # Main Project Window
2305
+ # ========================================================================
2306
+
2307
+ class ProjectWindow(QMainWindow):
2308
+ """Professional project interface main window"""
2309
+
2310
+ def __init__(self, project_path: str = None):
2311
+ super().__init__()
2312
+
2313
+ self.project_path = Path(project_path) if project_path else Path.cwd()
2314
+ self.current_file: Optional[str] = None
2315
+ self._drag_pos = None
2316
+ self._auto_save_timer = QTimer(self)
2317
+ self._auto_save_timer.timeout.connect(self._auto_save)
2318
+ self._auto_save_timer.start(30000)
2319
+
2320
+ self._setup_window()
2321
+ self._setup_ui()
2322
+
2323
+ def _setup_window(self):
2324
+ self.setWindowTitle("IncludeCPP - CodeMaker")
2325
+ self.setMinimumSize(1400, 900)
2326
+ self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
2327
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
2328
+
2329
+ def _setup_ui(self):
2330
+ container = QWidget()
2331
+ container.setStyleSheet(f'''
2332
+ QWidget {{
2333
+ background-color: {THEME['bg_primary']};
2334
+ border-radius: 12px;
2335
+ }}
2336
+ ''')
2337
+ self.setCentralWidget(container)
2338
+
2339
+ main_layout = QVBoxLayout(container)
2340
+ main_layout.setContentsMargins(0, 0, 0, 0)
2341
+ main_layout.setSpacing(0)
2342
+
2343
+ main_layout.addWidget(self._create_title_bar())
2344
+ main_layout.addWidget(self._create_toolbar())
2345
+
2346
+ content = QWidget()
2347
+ content_layout = QHBoxLayout(content)
2348
+ content_layout.setContentsMargins(0, 0, 0, 0)
2349
+ content_layout.setSpacing(0)
2350
+
2351
+ content_layout.addWidget(self._create_sidebar())
2352
+
2353
+ self.main_stack = QWidget()
2354
+ self.main_stack.setStyleSheet(f'background-color: {THEME["bg_dark"]};')
2355
+ self.main_stack_layout = QVBoxLayout(self.main_stack)
2356
+ self.main_stack_layout.setContentsMargins(0, 0, 0, 0)
2357
+ content_layout.addWidget(self.main_stack, 1)
2358
+
2359
+ main_layout.addWidget(content, 1)
2360
+ main_layout.addWidget(self._create_status_bar())
2361
+
2362
+ def _create_title_bar(self) -> QFrame:
2363
+ bar = QFrame()
2364
+ bar.setFixedHeight(52)
2365
+ bar.setStyleSheet(f'''
2366
+ QFrame {{
2367
+ background-color: {THEME['bg_secondary']};
2368
+ border-top-left-radius: 12px;
2369
+ border-top-right-radius: 12px;
2370
+ border-bottom: 1px solid {THEME['border']};
2371
+ }}
2372
+ ''')
2373
+
2374
+ layout = QHBoxLayout(bar)
2375
+ layout.setContentsMargins(20, 0, 16, 0)
2376
+
2377
+ logo = QLabel("IncludeCPP")
2378
+ logo.setFont(QFont(SYSTEM_FONT, 13, QFont.Weight.Bold))
2379
+ logo.setStyleSheet(f'color: {THEME["accent_blue"]};')
2380
+ layout.addWidget(logo)
2381
+
2382
+ sep = QLabel("|")
2383
+ sep.setStyleSheet(f'color: {THEME["border"]}; margin: 0 12px;')
2384
+ layout.addWidget(sep)
2385
+
2386
+ self.title_label = QLabel("CodeMaker")
2387
+ self.title_label.setFont(QFont(SYSTEM_FONT, 11))
2388
+ self.title_label.setStyleSheet(f'color: {THEME["text_secondary"]};')
2389
+ layout.addWidget(self.title_label)
2390
+
2391
+ exp_badge = QLabel("EXPERIMENTAL")
2392
+ exp_badge.setFont(QFont(SYSTEM_FONT, 8, QFont.Weight.Bold))
2393
+ exp_badge.setStyleSheet(f'''
2394
+ color: {THEME["accent_orange"]};
2395
+ background: {THEME["accent_orange"]}20;
2396
+ padding: 4px 8px;
2397
+ border-radius: 4px;
2398
+ margin-left: 12px;
2399
+ ''')
2400
+ layout.addWidget(exp_badge)
2401
+
2402
+ layout.addStretch()
2403
+
2404
+ for text, color, action in [
2405
+ ("−", THEME['bg_hover'], self.showMinimized),
2406
+ ("□", THEME['bg_hover'], self._toggle_maximize),
2407
+ ("×", THEME['accent_red'], self.close)
2408
+ ]:
2409
+ btn = QPushButton(text)
2410
+ btn.setFixedSize(40, 32)
2411
+ btn.setStyleSheet(f'''
2412
+ QPushButton {{
2413
+ background-color: transparent;
2414
+ border: none;
2415
+ border-radius: 6px;
2416
+ color: {THEME['text_secondary']};
2417
+ font-size: 16px;
2418
+ }}
2419
+ QPushButton:hover {{
2420
+ background-color: {color};
2421
+ color: {THEME['text_primary']};
2422
+ }}
2423
+ ''')
2424
+ btn.setCursor(Qt.CursorShape.PointingHandCursor)
2425
+ btn.clicked.connect(action)
2426
+ layout.addWidget(btn)
2427
+
2428
+ return bar
2429
+
2430
+ def _create_toolbar(self) -> QFrame:
2431
+ """Create toolbar with common actions."""
2432
+ toolbar = QFrame()
2433
+ toolbar.setFixedHeight(44)
2434
+ toolbar.setStyleSheet(f'''
2435
+ QFrame {{
2436
+ background-color: {THEME['bg_secondary']};
2437
+ border-bottom: 1px solid {THEME['border']};
2438
+ }}
2439
+ ''')
2440
+
2441
+ layout = QHBoxLayout(toolbar)
2442
+ layout.setContentsMargins(16, 4, 16, 4)
2443
+ layout.setSpacing(8)
2444
+
2445
+ btn_style = f'''
2446
+ QPushButton {{
2447
+ background: {THEME['bg_tertiary']};
2448
+ border: 1px solid {THEME['border']};
2449
+ border-radius: 6px;
2450
+ color: {THEME['text_primary']};
2451
+ padding: 6px 12px;
2452
+ font-size: 11px;
2453
+ }}
2454
+ QPushButton:hover {{
2455
+ background: {THEME['bg_hover']};
2456
+ border-color: {THEME['accent_blue']};
2457
+ }}
2458
+ '''
2459
+
2460
+ save_btn = QPushButton("Save")
2461
+ save_btn.setStyleSheet(btn_style)
2462
+ save_btn.clicked.connect(self._save_current)
2463
+ layout.addWidget(save_btn)
2464
+
2465
+ layout.addWidget(self._create_separator())
2466
+
2467
+ undo_btn = QPushButton("Undo")
2468
+ undo_btn.setStyleSheet(btn_style)
2469
+ undo_btn.clicked.connect(lambda: self.canvas.undo_stack.undo() if hasattr(self, 'canvas') else None)
2470
+ layout.addWidget(undo_btn)
2471
+
2472
+ redo_btn = QPushButton("Redo")
2473
+ redo_btn.setStyleSheet(btn_style)
2474
+ redo_btn.clicked.connect(lambda: self.canvas.undo_stack.redo() if hasattr(self, 'canvas') else None)
2475
+ layout.addWidget(redo_btn)
2476
+
2477
+ layout.addWidget(self._create_separator())
2478
+
2479
+ copy_btn = QPushButton("Copy")
2480
+ copy_btn.setStyleSheet(btn_style)
2481
+ copy_btn.clicked.connect(lambda: self.canvas.copy_selected() if hasattr(self, 'canvas') else None)
2482
+ layout.addWidget(copy_btn)
2483
+
2484
+ paste_btn = QPushButton("Paste")
2485
+ paste_btn.setStyleSheet(btn_style)
2486
+ paste_btn.clicked.connect(lambda: self.canvas.paste_nodes() if hasattr(self, 'canvas') else None)
2487
+ layout.addWidget(paste_btn)
2488
+
2489
+ layout.addStretch()
2490
+
2491
+ self.search_input = QLineEdit()
2492
+ self.search_input.setPlaceholderText("Search... (Ctrl+F)")
2493
+ self.search_input.setFixedWidth(200)
2494
+ self.search_input.setStyleSheet(f'''
2495
+ QLineEdit {{
2496
+ background: {THEME['bg_tertiary']};
2497
+ border: 1px solid {THEME['border']};
2498
+ border-radius: 6px;
2499
+ padding: 6px 12px;
2500
+ color: {THEME['text_primary']};
2501
+ }}
2502
+ QLineEdit:focus {{
2503
+ border-color: {THEME['accent_blue']};
2504
+ }}
2505
+ ''')
2506
+ self.search_input.textChanged.connect(
2507
+ lambda t: self.canvas.search_nodes(t) if hasattr(self, 'canvas') else None
2508
+ )
2509
+ layout.addWidget(self.search_input)
2510
+
2511
+ return toolbar
2512
+
2513
+ def _create_separator(self) -> QFrame:
2514
+ sep = QFrame()
2515
+ sep.setFixedSize(1, 24)
2516
+ sep.setStyleSheet(f'background: {THEME["border"]};')
2517
+ return sep
2518
+
2519
+ def _create_sidebar(self) -> QFrame:
2520
+ sidebar = QFrame()
2521
+ sidebar.setFixedWidth(240)
2522
+ sidebar.setStyleSheet(f'''
2523
+ QFrame {{
2524
+ background-color: {THEME['bg_primary']};
2525
+ border-right: 1px solid {THEME['border']};
2526
+ }}
2527
+ ''')
2528
+
2529
+ layout = QVBoxLayout(sidebar)
2530
+ layout.setContentsMargins(16, 20, 16, 20)
2531
+ layout.setSpacing(8)
2532
+
2533
+ section = QLabel("TOOLS")
2534
+ section.setFont(QFont(SYSTEM_FONT, 9, QFont.Weight.Bold))
2535
+ section.setStyleSheet(f'color: {THEME["text_muted"]}; letter-spacing: 1px;')
2536
+ layout.addWidget(section)
2537
+
2538
+ layout.addSpacing(8)
2539
+
2540
+ codemaker_btn = AnimatedButton("CodeMaker", "◈", THEME['accent_blue'])
2541
+ codemaker_btn.clicked.connect(self._show_codemaker)
2542
+ layout.addWidget(codemaker_btn)
2543
+
2544
+ layout.addStretch()
2545
+
2546
+ info_frame = QFrame()
2547
+ info_frame.setStyleSheet(f'''
2548
+ QFrame {{
2549
+ background-color: {THEME['bg_secondary']};
2550
+ border-radius: 8px;
2551
+ padding: 12px;
2552
+ }}
2553
+ ''')
2554
+ info_layout = QVBoxLayout(info_frame)
2555
+ info_layout.setContentsMargins(12, 12, 12, 12)
2556
+ info_layout.setSpacing(4)
2557
+
2558
+ proj_label = QLabel("PROJECT")
2559
+ proj_label.setFont(QFont(SYSTEM_FONT, 8))
2560
+ proj_label.setStyleSheet(f'color: {THEME["text_muted"]};')
2561
+ info_layout.addWidget(proj_label)
2562
+
2563
+ proj_name = QLabel(self.project_path.name)
2564
+ proj_name.setFont(QFont(SYSTEM_FONT, 10, QFont.Weight.Bold))
2565
+ proj_name.setStyleSheet(f'color: {THEME["text_primary"]};')
2566
+ proj_name.setWordWrap(True)
2567
+ info_layout.addWidget(proj_name)
2568
+
2569
+ layout.addWidget(info_frame)
2570
+
2571
+ return sidebar
2572
+
2573
+ def _create_status_bar(self) -> QFrame:
2574
+ bar = QFrame()
2575
+ bar.setFixedHeight(28)
2576
+ bar.setStyleSheet(f'''
2577
+ QFrame {{
2578
+ background-color: {THEME['bg_secondary']};
2579
+ border-bottom-left-radius: 12px;
2580
+ border-bottom-right-radius: 12px;
2581
+ border-top: 1px solid {THEME['border']};
2582
+ }}
2583
+ ''')
2584
+
2585
+ layout = QHBoxLayout(bar)
2586
+ layout.setContentsMargins(16, 0, 16, 0)
2587
+
2588
+ self.status_label = QLabel("Ready")
2589
+ self.status_label.setFont(QFont(SYSTEM_FONT, 9))
2590
+ self.status_label.setStyleSheet(f'color: {THEME["text_muted"]};')
2591
+ layout.addWidget(self.status_label)
2592
+
2593
+ layout.addStretch()
2594
+
2595
+ self.node_count_label = QLabel("Nodes: 0")
2596
+ self.node_count_label.setStyleSheet(f'color: {THEME["text_muted"]}; margin-right: 16px;')
2597
+ layout.addWidget(self.node_count_label)
2598
+
2599
+ self.connection_count_label = QLabel("Connections: 0")
2600
+ self.connection_count_label.setStyleSheet(f'color: {THEME["text_muted"]}; margin-right: 16px;')
2601
+ layout.addWidget(self.connection_count_label)
2602
+
2603
+ self.zoom_label = QLabel("Zoom: 100%")
2604
+ self.zoom_label.setStyleSheet(f'color: {THEME["text_muted"]};')
2605
+ layout.addWidget(self.zoom_label)
2606
+
2607
+ return bar
2608
+
2609
+ def _show_codemaker(self):
2610
+ """Display CodeMaker interface"""
2611
+ while self.main_stack_layout.count():
2612
+ item = self.main_stack_layout.takeAt(0)
2613
+ if item.widget():
2614
+ item.widget().deleteLater()
2615
+
2616
+ self.title_label.setText("CodeMaker")
2617
+
2618
+ codemaker = QWidget()
2619
+ codemaker_layout = QHBoxLayout(codemaker)
2620
+ codemaker_layout.setContentsMargins(0, 0, 0, 0)
2621
+ codemaker_layout.setSpacing(0)
2622
+
2623
+ self.file_tree = FileTreePanel(self.project_path)
2624
+ self.file_tree.setFixedWidth(220)
2625
+ self.file_tree.file_selected.connect(self._load_map)
2626
+ codemaker_layout.addWidget(self.file_tree)
2627
+
2628
+ self.canvas = CodeMakerCanvas()
2629
+ self.canvas.node_count_changed.connect(
2630
+ lambda n: self.node_count_label.setText(f"Nodes: {n}")
2631
+ )
2632
+ self.canvas.connection_count_changed.connect(
2633
+ lambda n: self.connection_count_label.setText(f"Connections: {n}")
2634
+ )
2635
+ self.canvas.status_message.connect(
2636
+ lambda msg: self.status_label.setText(msg)
2637
+ )
2638
+ codemaker_layout.addWidget(self.canvas, 1)
2639
+
2640
+ self.main_stack_layout.addWidget(codemaker)
2641
+
2642
+ def _load_map(self, file_path: str):
2643
+ """Load a map file"""
2644
+ try:
2645
+ data = json.loads(Path(file_path).read_text(encoding='utf-8'))
2646
+ map_data = MapData.from_dict(data)
2647
+ self.canvas.load_map_data(map_data)
2648
+ self.current_file = file_path
2649
+ self.title_label.setText(f"CodeMaker - {map_data.name}")
2650
+ self.status_label.setText(f"Loaded: {Path(file_path).stem}")
2651
+ except Exception as e:
2652
+ QMessageBox.warning(self, "Error", f"Failed to load map: {e}")
2653
+
2654
+ def _save_current(self):
2655
+ """Save current map"""
2656
+ if self.current_file and hasattr(self, 'canvas'):
2657
+ try:
2658
+ map_data = self.canvas.get_map_data()
2659
+ map_data.name = Path(self.current_file).stem
2660
+ Path(self.current_file).write_text(
2661
+ json.dumps(map_data.to_dict(), indent=2),
2662
+ encoding='utf-8'
2663
+ )
2664
+ self.status_label.setText(f"Saved: {map_data.name}")
2665
+ except Exception as e:
2666
+ self.status_label.setText(f"Save failed: {e}")
2667
+
2668
+ def _auto_save(self):
2669
+ """Auto-save callback"""
2670
+ if self.current_file and hasattr(self, 'canvas'):
2671
+ self._save_current()
2672
+
2673
+ def _toggle_maximize(self):
2674
+ if self.isMaximized():
2675
+ self.showNormal()
2676
+ else:
2677
+ self.showMaximized()
2678
+
2679
+ def mousePressEvent(self, event):
2680
+ if event.button() == Qt.MouseButton.LeftButton and event.position().y() < 52:
2681
+ self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
2682
+
2683
+ def mouseMoveEvent(self, event):
2684
+ if event.buttons() == Qt.MouseButton.LeftButton and self._drag_pos:
2685
+ self.move(event.globalPosition().toPoint() - self._drag_pos)
2686
+
2687
+ def mouseReleaseEvent(self, event):
2688
+ self._drag_pos = None
2689
+
2690
+ def closeEvent(self, event):
2691
+ self._save_current()
2692
+ super().closeEvent(event)
2693
+
2694
+
2695
+ def show_project(project_path: str = None) -> Tuple[bool, str]:
2696
+ """Launch the project interface"""
2697
+ if not PYQT_AVAILABLE:
2698
+ return False, "PyQt6 not installed. Run: pip install PyQt6"
2699
+
2700
+ app = QApplication.instance()
2701
+ if not app:
2702
+ app = QApplication(sys.argv)
2703
+
2704
+ window = ProjectWindow(project_path)
2705
+ window.show()
2706
+ window._show_codemaker()
2707
+
2708
+ app.exec()
2709
+ return True, "Project closed"
2710
+
2711
+
2712
+ else:
2713
+ def show_project(project_path: str = None) -> Tuple[bool, str]:
2714
+ return False, "PyQt6 not installed. Run: pip install PyQt6"
2715
+
2716
+
2717
+ if __name__ == '__main__':
2718
+ success, msg = show_project()
2719
+ if not success:
2720
+ print(msg)