IncludeCPP 3.3.20__py3-none-any.whl → 3.4.8__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.
@@ -134,6 +134,7 @@ THEME = {
134
134
 
135
135
  # Expanded node types with categories
136
136
  NODE_CATEGORIES = {
137
+ 'Sources': ['source'], # Primary - generates actual code files
137
138
  'Code Structures': ['class', 'struct', 'enum', 'interface', 'template_class', 'union'],
138
139
  'Functions': ['function', 'method', 'constructor', 'destructor', 'lambda', 'operator'],
139
140
  'Data': ['object', 'variable', 'constant', 'definition', 'typedef', 'pointer'],
@@ -142,6 +143,9 @@ NODE_CATEGORIES = {
142
143
  }
143
144
 
144
145
  NODE_TYPES = {
146
+ # Sources - Primary nodes that generate actual code files
147
+ 'source': {'color': '#00e676', 'icon': 'S', 'gradient_start': '#00ff88', 'gradient_end': '#00c853', 'label': 'Source', 'category': 'Sources', 'port_count': 8},
148
+
145
149
  # Code Structures
146
150
  'class': {'color': '#4a9eff', 'icon': 'C', 'gradient_start': '#5aaeff', 'gradient_end': '#3a8eef', 'label': 'Class', 'category': 'Code Structures'},
147
151
  'struct': {'color': '#9c27b0', 'icon': 'S', 'gradient_start': '#ac37c0', 'gradient_end': '#8c17a0', 'label': 'Struct', 'category': 'Code Structures'},
@@ -217,6 +221,7 @@ class NodeData:
217
221
  properties: Dict[str, Any] = field(default_factory=dict)
218
222
  ports: List[Dict] = field(default_factory=list)
219
223
  group_id: str = ""
224
+ generated_files: Dict[str, str] = field(default_factory=dict) # 'python': path, 'plugin': path
220
225
  created_at: str = ""
221
226
  updated_at: str = ""
222
227
 
@@ -583,36 +588,75 @@ if PYQT_AVAILABLE:
583
588
  type_rect = self.type_label.boundingRect()
584
589
  self.type_label.setPos(w - type_rect.width() - 10, 10)
585
590
 
591
+ # Description area with background (always created, hidden if empty)
592
+ self.desc_bg = QGraphicsRectItem(5, 36, w - 10, h - 44, self)
593
+ self.desc_bg.setBrush(QBrush(QColor(0, 0, 0, 40)))
594
+ self.desc_bg.setPen(QPen(Qt.PenStyle.NoPen))
595
+
596
+ self.desc_label = QGraphicsTextItem("", self)
597
+ self.desc_label.setDefaultTextColor(QColor(255, 255, 255, 200))
598
+ self.desc_label.setFont(QFont(SYSTEM_FONT, 9))
599
+ self.desc_label.setTextWidth(w - 20)
600
+ self.desc_label.setPos(10, 38)
601
+
586
602
  if self.node_data.description:
587
- desc_text = self.node_data.description[:80]
588
- if len(self.node_data.description) > 80:
603
+ desc_text = self.node_data.description[:120]
604
+ if len(self.node_data.description) > 120:
589
605
  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)
606
+ self.desc_label.setPlainText(desc_text)
607
+ self.desc_bg.setVisible(True)
608
+ else:
609
+ self.desc_bg.setVisible(False)
595
610
 
611
+ # Connection points - 8 for source nodes, 2 for others
596
612
  point_size = 10
597
613
  self.connection_points = []
598
614
 
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))
615
+ port_count = self.node_theme.get('port_count', 2)
616
+
617
+ if port_count == 8:
618
+ # Source node: 4 ports on each side
619
+ for i in range(4):
620
+ y_pos = 20 + (h - 40) * (i + 0.5) / 4
621
+
622
+ # Right side ports
623
+ right_point = QGraphicsEllipseItem(
624
+ w - point_size/2, y_pos - point_size/2,
625
+ point_size, point_size, self
626
+ )
627
+ right_point.setBrush(QBrush(QColor(THEME['bg_dark'])))
628
+ right_point.setPen(QPen(self.base_color, 2))
629
+ right_point.setZValue(2)
630
+ self.connection_points.append((f'right_{i}', right_point))
631
+
632
+ # Left side ports
633
+ left_point = QGraphicsEllipseItem(
634
+ -point_size/2, y_pos - point_size/2,
635
+ point_size, point_size, self
636
+ )
637
+ left_point.setBrush(QBrush(QColor(THEME['bg_dark'])))
638
+ left_point.setPen(QPen(self.base_color, 2))
639
+ left_point.setZValue(2)
640
+ self.connection_points.append((f'left_{i}', left_point))
641
+ else:
642
+ # Standard node: 1 port on each side
643
+ right_point = QGraphicsEllipseItem(
644
+ w - point_size/2, h/2 - point_size/2,
645
+ point_size, point_size, self
646
+ )
647
+ right_point.setBrush(QBrush(QColor(THEME['bg_dark'])))
648
+ right_point.setPen(QPen(self.base_color, 2))
649
+ right_point.setZValue(2)
650
+ self.connection_points.append(('right', right_point))
651
+
652
+ left_point = QGraphicsEllipseItem(
653
+ -point_size/2, h/2 - point_size/2,
654
+ point_size, point_size, self
655
+ )
656
+ left_point.setBrush(QBrush(QColor(THEME['bg_dark'])))
657
+ left_point.setPen(QPen(self.base_color, 2))
658
+ left_point.setZValue(2)
659
+ self.connection_points.append(('left', left_point))
616
660
 
617
661
  self.bottom_line = QGraphicsRectItem(10, h - 4, w - 20, 2, self)
618
662
  self.bottom_line.setBrush(QBrush(self.base_color.lighter(130)))
@@ -715,8 +759,10 @@ if PYQT_AVAILABLE:
715
759
  """Update the description"""
716
760
  self.node_data.description = desc
717
761
  if hasattr(self, 'desc_label'):
718
- display_text = desc[:80] + "..." if len(desc) > 80 else desc
762
+ display_text = desc[:120] + "..." if len(desc) > 120 else desc
719
763
  self.desc_label.setPlainText(display_text)
764
+ if hasattr(self, 'desc_bg'):
765
+ self.desc_bg.setVisible(bool(desc))
720
766
  self.node_data.updated_at = datetime.now().isoformat()
721
767
 
722
768
 
@@ -1864,6 +1910,8 @@ if PYQT_AVAILABLE:
1864
1910
  super().mouseReleaseEvent(event)
1865
1911
 
1866
1912
  def keyPressEvent(self, event: QKeyEvent):
1913
+ move_amount = 50 # Grid step for arrow navigation
1914
+
1867
1915
  if event.key() == Qt.Key.Key_Escape:
1868
1916
  if self._connecting_mode:
1869
1917
  self.cancel_connection_mode()
@@ -1878,6 +1926,15 @@ if PYQT_AVAILABLE:
1878
1926
  elif event.key() == Qt.Key.Key_0 and event.modifiers() & Qt.KeyboardModifier.ControlModifier:
1879
1927
  self._zoom_level = 1.0
1880
1928
  self.resetTransform()
1929
+ # Arrow key navigation
1930
+ elif event.key() == Qt.Key.Key_Up:
1931
+ self.verticalScrollBar().setValue(self.verticalScrollBar().value() - move_amount)
1932
+ elif event.key() == Qt.Key.Key_Down:
1933
+ self.verticalScrollBar().setValue(self.verticalScrollBar().value() + move_amount)
1934
+ elif event.key() == Qt.Key.Key_Left:
1935
+ self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - move_amount)
1936
+ elif event.key() == Qt.Key.Key_Right:
1937
+ self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + move_amount)
1881
1938
  else:
1882
1939
  super().keyPressEvent(event)
1883
1940
 
@@ -1937,6 +1994,7 @@ if PYQT_AVAILABLE:
1937
1994
  if node:
1938
1995
  props_action = menu.addAction("Properties...")
1939
1996
  rename_action = menu.addAction("Rename")
1997
+ desc_action = menu.addAction("Add Description...")
1940
1998
  menu.addSeparator()
1941
1999
 
1942
2000
  conn_menu = menu.addMenu("Connections")
@@ -1952,11 +2010,34 @@ if PYQT_AVAILABLE:
1952
2010
 
1953
2011
  menu.addSeparator()
1954
2012
 
2013
+ # Code menu - different options for source nodes
1955
2014
  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")
2015
+ create_python = None
2016
+ create_plugin = None
2017
+ preview_code = None
2018
+ gen_cpp_h = None
2019
+ gen_cpp_cpp = None
2020
+ gen_python = None
2021
+
2022
+ if node.node_data.node_type == 'source':
2023
+ # Source nodes get file generation options
2024
+ if not node.node_data.generated_files.get('python'):
2025
+ create_python = code_menu.addAction("Create Python")
2026
+ if not node.node_data.generated_files.get('plugin'):
2027
+ create_plugin = code_menu.addAction("Create Plugin")
2028
+
2029
+ if node.node_data.generated_files.get('python') or node.node_data.generated_files.get('plugin'):
2030
+ code_menu.addSeparator()
2031
+ if node.node_data.generated_files.get('python'):
2032
+ code_menu.addAction(f"Python: {Path(node.node_data.generated_files['python']).name}").setEnabled(False)
2033
+ if node.node_data.generated_files.get('plugin'):
2034
+ code_menu.addAction(f"Plugin: {Path(node.node_data.generated_files['plugin']).name}").setEnabled(False)
2035
+ else:
2036
+ # Non-source nodes get preview options
2037
+ preview_code = code_menu.addAction("Preview Code...")
2038
+ gen_cpp_h = code_menu.addAction("Generate C++ Header")
2039
+ gen_cpp_cpp = code_menu.addAction("Generate C++ Source")
2040
+ gen_python = code_menu.addAction("Generate Python")
1960
2041
 
1961
2042
  menu.addSeparator()
1962
2043
  delete_action = menu.addAction("Delete")
@@ -1978,6 +2059,14 @@ if PYQT_AVAILABLE:
1978
2059
  if ok and name:
1979
2060
  node.update_name(name)
1980
2061
 
2062
+ elif action == desc_action:
2063
+ desc, ok = QInputDialog.getMultiLineText(
2064
+ self, "Description", "Enter description:",
2065
+ text=node.node_data.description
2066
+ )
2067
+ if ok:
2068
+ node.update_description(desc)
2069
+
1981
2070
  elif action == connect_action:
1982
2071
  self.start_connection_mode(node)
1983
2072
 
@@ -1997,21 +2086,27 @@ if PYQT_AVAILABLE:
1997
2086
  node.setSelected(True)
1998
2087
  self.duplicate_selected()
1999
2088
 
2000
- elif action == preview_code:
2089
+ elif action == create_python and create_python:
2090
+ self._create_python_file(node)
2091
+
2092
+ elif action == create_plugin and create_plugin:
2093
+ self._create_plugin_files(node)
2094
+
2095
+ elif action == preview_code and preview_code:
2001
2096
  dialog = CodePreviewDialog(node, self)
2002
2097
  dialog.exec()
2003
2098
 
2004
- elif action == gen_cpp_h:
2099
+ elif action == gen_cpp_h and gen_cpp_h:
2005
2100
  code = CodeGenerator.generate_cpp_header(node.node_data)
2006
2101
  QApplication.clipboard().setText(code)
2007
2102
  self.status_message.emit("C++ header copied to clipboard")
2008
2103
 
2009
- elif action == gen_cpp_cpp:
2104
+ elif action == gen_cpp_cpp and gen_cpp_cpp:
2010
2105
  code = CodeGenerator.generate_cpp_source(node.node_data)
2011
2106
  QApplication.clipboard().setText(code)
2012
2107
  self.status_message.emit("C++ source copied to clipboard")
2013
2108
 
2014
- elif action == gen_python:
2109
+ elif action == gen_python and gen_python:
2015
2110
  code = CodeGenerator.generate_python(node.node_data)
2016
2111
  QApplication.clipboard().setText(code)
2017
2112
  self.status_message.emit("Python code copied to clipboard")
@@ -2020,6 +2115,10 @@ if PYQT_AVAILABLE:
2020
2115
  self.delete_node(node)
2021
2116
 
2022
2117
  else:
2118
+ # Quick Source creation at top
2119
+ create_source_action = menu.addAction("+ Create New Source")
2120
+ menu.addSeparator()
2121
+
2023
2122
  create_menu = menu.addMenu("New")
2024
2123
  self._create_categorized_menu(create_menu, scene_pos)
2025
2124
 
@@ -2052,7 +2151,12 @@ if PYQT_AVAILABLE:
2052
2151
 
2053
2152
  action = menu.exec(event.globalPos())
2054
2153
 
2055
- if action and hasattr(action, 'data') and action.data():
2154
+ if action == create_source_action:
2155
+ name, ok = QInputDialog.getText(self, "New Source", "Source name:")
2156
+ if ok and name:
2157
+ self.add_node('source', name, scene_pos.x(), scene_pos.y())
2158
+
2159
+ elif action and hasattr(action, 'data') and action.data():
2056
2160
  data = action.data()
2057
2161
  if isinstance(data, tuple):
2058
2162
  node_type, pos = data
@@ -2104,6 +2208,441 @@ if PYQT_AVAILABLE:
2104
2208
  if file_path:
2105
2209
  self.export_to_image(file_path, "SVG")
2106
2210
 
2211
+ def _get_connected_nodes(self, source_node: VisualNode) -> List[VisualNode]:
2212
+ """Get all nodes connected to this source node (traverses full graph)."""
2213
+ connected = []
2214
+ visited = set()
2215
+
2216
+ def traverse(node):
2217
+ if node.node_data.id in visited:
2218
+ return
2219
+ visited.add(node.node_data.id)
2220
+ if node != source_node:
2221
+ connected.append(node)
2222
+ for conn in node.connections:
2223
+ other = conn.end_node if conn.start_node == node else conn.start_node
2224
+ traverse(other)
2225
+
2226
+ traverse(source_node)
2227
+ return connected
2228
+
2229
+ def _create_python_file(self, node: VisualNode):
2230
+ """Create Python file from source node with all connected nodes."""
2231
+ name = node.node_data.name.replace(' ', '_').lower()
2232
+ project_path = self._get_project_path()
2233
+ file_path = project_path / f"{name}.py"
2234
+
2235
+ connected = self._get_connected_nodes(node)
2236
+
2237
+ # Build code from connections
2238
+ code_parts = [
2239
+ f'"""',
2240
+ f'{node.node_data.name}',
2241
+ f'{node.node_data.description or "Generated by IncludeCPP CodeMaker"}',
2242
+ f'"""',
2243
+ ''
2244
+ ]
2245
+
2246
+ # Generate classes first
2247
+ for n in connected:
2248
+ if n.node_data.node_type in ('class', 'struct', 'interface', 'enum'):
2249
+ code_parts.append(CodeGenerator.generate_python(n.node_data))
2250
+ code_parts.append('')
2251
+
2252
+ # Then functions
2253
+ for n in connected:
2254
+ if n.node_data.node_type in ('function', 'method', 'lambda', 'constructor', 'destructor'):
2255
+ code_parts.append(CodeGenerator.generate_python(n.node_data))
2256
+ code_parts.append('')
2257
+
2258
+ # Main entry point
2259
+ code_parts.append('if __name__ == "__main__":')
2260
+ code_parts.append(' pass # TODO: Entry point')
2261
+
2262
+ try:
2263
+ file_path.write_text('\n'.join(code_parts), encoding='utf-8')
2264
+ node.node_data.generated_files['python'] = str(file_path)
2265
+ self.status_message.emit(f"Created: {file_path.name}")
2266
+ except Exception as e:
2267
+ self.status_message.emit(f"Error: {e}")
2268
+
2269
+ def _create_plugin_files(self, node: VisualNode):
2270
+ """Create Plugin files from source node with all connected nodes."""
2271
+ name = node.node_data.name.replace(' ', '_').lower()
2272
+ project_path = self._get_project_path()
2273
+ connected = self._get_connected_nodes(node)
2274
+
2275
+ # Create directories
2276
+ plugins_dir = project_path / "plugins"
2277
+ include_dir = project_path / "include"
2278
+ plugins_dir.mkdir(exist_ok=True)
2279
+ include_dir.mkdir(exist_ok=True)
2280
+
2281
+ # Gather connected code
2282
+ classes = [n for n in connected if n.node_data.node_type in ('class', 'struct', 'interface', 'enum')]
2283
+ functions = [n for n in connected if n.node_data.node_type in ('function', 'method', 'constructor', 'destructor', 'lambda', 'operator')]
2284
+
2285
+ try:
2286
+ # .h file (include/) - declarations for all connected nodes
2287
+ h_parts = [
2288
+ '#pragma once',
2289
+ f'// {node.node_data.name} - Generated by IncludeCPP CodeMaker',
2290
+ f'// {node.node_data.description or ""}',
2291
+ ''
2292
+ ]
2293
+ for c in classes:
2294
+ h_parts.append(CodeGenerator.generate_cpp_header(c.node_data))
2295
+ h_parts.append('')
2296
+ for f in functions:
2297
+ h_parts.append(CodeGenerator.generate_cpp_header(f.node_data))
2298
+ h_parts.append('')
2299
+
2300
+ h_file = include_dir / f"{name}.h"
2301
+ h_file.write_text('\n'.join(h_parts), encoding='utf-8')
2302
+
2303
+ # .cpp file (include/) - implementations
2304
+ cpp_parts = [f'#include "{name}.h"', '']
2305
+ for c in classes:
2306
+ cpp_parts.append(CodeGenerator.generate_cpp_source(c.node_data))
2307
+ cpp_parts.append('')
2308
+ for f in functions:
2309
+ cpp_parts.append(CodeGenerator.generate_cpp_source(f.node_data))
2310
+ cpp_parts.append('')
2311
+
2312
+ cpp_file = include_dir / f"{name}.cpp"
2313
+ cpp_file.write_text('\n'.join(cpp_parts), encoding='utf-8')
2314
+
2315
+ # .cp file (plugins/) - IncludeCPP plugin entry
2316
+ connected_names = ', '.join(n.node_data.name for n in connected) if connected else 'None'
2317
+ cp_content = f'''// {node.node_data.name} - IncludeCPP Plugin
2318
+ // {node.node_data.description or 'Generated by CodeMaker'}
2319
+ // Connected elements: {connected_names}
2320
+
2321
+ #include "{name}.h"
2322
+
2323
+ void {name}_init() {{
2324
+ // Plugin initialization
2325
+ }}
2326
+ '''
2327
+ cp_file = plugins_dir / f"{name}.cp"
2328
+ cp_file.write_text(cp_content, encoding='utf-8')
2329
+
2330
+ node.node_data.generated_files['plugin'] = str(cp_file)
2331
+ self.status_message.emit(f"Created plugin: {name}.cp, {name}.h, {name}.cpp")
2332
+ except Exception as e:
2333
+ self.status_message.emit(f"Error: {e}")
2334
+
2335
+ def _get_project_path(self) -> Path:
2336
+ """Get the project path from parent window."""
2337
+ for view in self.scene().views():
2338
+ if hasattr(view, 'window') and view.window():
2339
+ win = view.window()
2340
+ if hasattr(win, 'project_path'):
2341
+ return win.project_path
2342
+ return Path.cwd()
2343
+
2344
+ def align_horizontal(self):
2345
+ """Align selected nodes horizontally (same Y position)."""
2346
+ selected = [item for item in self.scene.selectedItems() if isinstance(item, VisualNode)]
2347
+ if len(selected) < 2:
2348
+ self.status_message.emit("Select at least 2 nodes to align")
2349
+ return
2350
+
2351
+ # Use average Y position
2352
+ avg_y = sum(n.pos().y() for n in selected) / len(selected)
2353
+ for node in selected:
2354
+ node.setPos(node.pos().x(), avg_y)
2355
+ node.node_data.y = avg_y
2356
+
2357
+ self.status_message.emit(f"Aligned {len(selected)} nodes horizontally")
2358
+
2359
+ def align_vertical(self):
2360
+ """Align selected nodes vertically (same X position)."""
2361
+ selected = [item for item in self.scene.selectedItems() if isinstance(item, VisualNode)]
2362
+ if len(selected) < 2:
2363
+ self.status_message.emit("Select at least 2 nodes to align")
2364
+ return
2365
+
2366
+ # Use average X position
2367
+ avg_x = sum(n.pos().x() for n in selected) / len(selected)
2368
+ for node in selected:
2369
+ node.setPos(avg_x, node.pos().y())
2370
+ node.node_data.x = avg_x
2371
+
2372
+ self.status_message.emit(f"Aligned {len(selected)} nodes vertically")
2373
+
2374
+ def auto_arrange(self):
2375
+ """Automatically arrange all nodes in a grid layout."""
2376
+ if not self.nodes:
2377
+ return
2378
+
2379
+ nodes_list = list(self.nodes.values())
2380
+ n = len(nodes_list)
2381
+
2382
+ # Calculate grid dimensions
2383
+ cols = max(1, int(n ** 0.5))
2384
+ rows = (n + cols - 1) // cols
2385
+
2386
+ spacing_x = 250
2387
+ spacing_y = 150
2388
+ start_x = -((cols - 1) * spacing_x) / 2
2389
+ start_y = -((rows - 1) * spacing_y) / 2
2390
+
2391
+ for i, node in enumerate(nodes_list):
2392
+ col = i % cols
2393
+ row = i // cols
2394
+ x = start_x + col * spacing_x
2395
+ y = start_y + row * spacing_y
2396
+ node.setPos(x, y)
2397
+ node.node_data.x = x
2398
+ node.node_data.y = y
2399
+
2400
+ self.status_message.emit(f"Arranged {n} nodes in {rows}x{cols} grid")
2401
+
2402
+ def distribute_horizontal(self):
2403
+ """Distribute selected nodes evenly horizontally."""
2404
+ selected = sorted(
2405
+ [item for item in self.scene.selectedItems() if isinstance(item, VisualNode)],
2406
+ key=lambda n: n.pos().x()
2407
+ )
2408
+ if len(selected) < 3:
2409
+ self.status_message.emit("Select at least 3 nodes to distribute")
2410
+ return
2411
+
2412
+ min_x = selected[0].pos().x()
2413
+ max_x = selected[-1].pos().x()
2414
+ spacing = (max_x - min_x) / (len(selected) - 1)
2415
+
2416
+ for i, node in enumerate(selected):
2417
+ new_x = min_x + i * spacing
2418
+ node.setPos(new_x, node.pos().y())
2419
+ node.node_data.x = new_x
2420
+
2421
+ self.status_message.emit(f"Distributed {len(selected)} nodes horizontally")
2422
+
2423
+
2424
+ # ========================================================================
2425
+ # Properties Panel
2426
+ # ========================================================================
2427
+
2428
+ class PropertiesPanel(QFrame):
2429
+ """Dockable panel for editing selected node properties."""
2430
+
2431
+ property_changed = pyqtSignal(str, str, object) # node_id, property, value
2432
+
2433
+ def __init__(self, parent=None):
2434
+ super().__init__(parent)
2435
+ self.current_node = None
2436
+ self._setup_ui()
2437
+
2438
+ def _setup_ui(self):
2439
+ self.setFixedWidth(260)
2440
+ self.setStyleSheet(f'''
2441
+ QFrame {{
2442
+ background-color: {THEME['bg_primary']};
2443
+ border-left: 1px solid {THEME['border']};
2444
+ }}
2445
+ ''')
2446
+
2447
+ layout = QVBoxLayout(self)
2448
+ layout.setContentsMargins(0, 0, 0, 0)
2449
+ layout.setSpacing(0)
2450
+
2451
+ # Header
2452
+ header = QFrame()
2453
+ header.setFixedHeight(40)
2454
+ header.setStyleSheet(f'''
2455
+ QFrame {{
2456
+ background-color: {THEME['bg_secondary']};
2457
+ border-bottom: 1px solid {THEME['border']};
2458
+ }}
2459
+ ''')
2460
+ header_layout = QHBoxLayout(header)
2461
+ header_layout.setContentsMargins(12, 0, 12, 0)
2462
+
2463
+ title = QLabel("PROPERTIES")
2464
+ title.setFont(QFont(SYSTEM_FONT, 9, QFont.Weight.Bold))
2465
+ title.setStyleSheet(f'color: {THEME["text_muted"]}; letter-spacing: 1px;')
2466
+ header_layout.addWidget(title)
2467
+ layout.addWidget(header)
2468
+
2469
+ # Content area (scrollable)
2470
+ scroll = QScrollArea()
2471
+ scroll.setWidgetResizable(True)
2472
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
2473
+ scroll.setStyleSheet(f'''
2474
+ QScrollArea {{
2475
+ background-color: {THEME['bg_primary']};
2476
+ border: none;
2477
+ }}
2478
+ ''')
2479
+
2480
+ content = QWidget()
2481
+ content.setStyleSheet(f'background-color: {THEME["bg_primary"]};')
2482
+ self.content_layout = QVBoxLayout(content)
2483
+ self.content_layout.setContentsMargins(12, 12, 12, 12)
2484
+ self.content_layout.setSpacing(12)
2485
+
2486
+ # Placeholder text
2487
+ self.placeholder = QLabel("Select a node to edit its properties")
2488
+ self.placeholder.setStyleSheet(f'color: {THEME["text_muted"]}; font-style: italic;')
2489
+ self.placeholder.setWordWrap(True)
2490
+ self.content_layout.addWidget(self.placeholder)
2491
+
2492
+ # Name field
2493
+ self.name_label = QLabel("Name")
2494
+ self.name_label.setStyleSheet(f'color: {THEME["text_secondary"]}; font-size: 10px;')
2495
+ self.name_label.hide()
2496
+ self.content_layout.addWidget(self.name_label)
2497
+
2498
+ self.name_input = QLineEdit()
2499
+ self.name_input.setStyleSheet(f'''
2500
+ QLineEdit {{
2501
+ background: {THEME['bg_tertiary']};
2502
+ border: 1px solid {THEME['border']};
2503
+ border-radius: 4px;
2504
+ padding: 6px 8px;
2505
+ color: {THEME['text_primary']};
2506
+ }}
2507
+ QLineEdit:focus {{ border-color: {THEME['accent_blue']}; }}
2508
+ ''')
2509
+ self.name_input.hide()
2510
+ self.name_input.textChanged.connect(self._on_name_changed)
2511
+ self.content_layout.addWidget(self.name_input)
2512
+
2513
+ # Type display
2514
+ self.type_label = QLabel("Type")
2515
+ self.type_label.setStyleSheet(f'color: {THEME["text_secondary"]}; font-size: 10px;')
2516
+ self.type_label.hide()
2517
+ self.content_layout.addWidget(self.type_label)
2518
+
2519
+ self.type_display = QLabel("")
2520
+ self.type_display.setStyleSheet(f'color: {THEME["text_primary"]}; font-weight: bold;')
2521
+ self.type_display.hide()
2522
+ self.content_layout.addWidget(self.type_display)
2523
+
2524
+ # Description field
2525
+ self.desc_label = QLabel("Description")
2526
+ self.desc_label.setStyleSheet(f'color: {THEME["text_secondary"]}; font-size: 10px;')
2527
+ self.desc_label.hide()
2528
+ self.content_layout.addWidget(self.desc_label)
2529
+
2530
+ self.desc_input = QTextEdit()
2531
+ self.desc_input.setMaximumHeight(80)
2532
+ self.desc_input.setStyleSheet(f'''
2533
+ QTextEdit {{
2534
+ background: {THEME['bg_tertiary']};
2535
+ border: 1px solid {THEME['border']};
2536
+ border-radius: 4px;
2537
+ padding: 6px 8px;
2538
+ color: {THEME['text_primary']};
2539
+ }}
2540
+ QTextEdit:focus {{ border-color: {THEME['accent_blue']}; }}
2541
+ ''')
2542
+ self.desc_input.hide()
2543
+ self.desc_input.textChanged.connect(self._on_desc_changed)
2544
+ self.content_layout.addWidget(self.desc_input)
2545
+
2546
+ # Code status (for source nodes)
2547
+ self.code_section = QFrame()
2548
+ self.code_section.setStyleSheet(f'''
2549
+ QFrame {{
2550
+ background: {THEME['bg_secondary']};
2551
+ border-radius: 6px;
2552
+ padding: 8px;
2553
+ }}
2554
+ ''')
2555
+ code_layout = QVBoxLayout(self.code_section)
2556
+ code_layout.setContentsMargins(8, 8, 8, 8)
2557
+ code_layout.setSpacing(4)
2558
+
2559
+ code_title = QLabel("CODE STATUS")
2560
+ code_title.setStyleSheet(f'color: {THEME["text_muted"]}; font-size: 9px;')
2561
+ code_layout.addWidget(code_title)
2562
+
2563
+ self.python_status = QLabel("Python: Not created")
2564
+ self.python_status.setStyleSheet(f'color: {THEME["text_secondary"]};')
2565
+ code_layout.addWidget(self.python_status)
2566
+
2567
+ self.plugin_status = QLabel("Plugin: Not created")
2568
+ self.plugin_status.setStyleSheet(f'color: {THEME["text_secondary"]};')
2569
+ code_layout.addWidget(self.plugin_status)
2570
+
2571
+ self.code_section.hide()
2572
+ self.content_layout.addWidget(self.code_section)
2573
+
2574
+ self.content_layout.addStretch()
2575
+
2576
+ scroll.setWidget(content)
2577
+ layout.addWidget(scroll)
2578
+
2579
+ def update_selection(self, node: Optional[VisualNode]):
2580
+ """Update panel to show properties of selected node."""
2581
+ self.current_node = node
2582
+
2583
+ if node is None:
2584
+ self.placeholder.show()
2585
+ self.name_label.hide()
2586
+ self.name_input.hide()
2587
+ self.type_label.hide()
2588
+ self.type_display.hide()
2589
+ self.desc_label.hide()
2590
+ self.desc_input.hide()
2591
+ self.code_section.hide()
2592
+ return
2593
+
2594
+ self.placeholder.hide()
2595
+
2596
+ # Show all fields
2597
+ self.name_label.show()
2598
+ self.name_input.show()
2599
+ self.type_label.show()
2600
+ self.type_display.show()
2601
+ self.desc_label.show()
2602
+ self.desc_input.show()
2603
+
2604
+ # Update values
2605
+ self.name_input.blockSignals(True)
2606
+ self.name_input.setText(node.node_data.name)
2607
+ self.name_input.blockSignals(False)
2608
+
2609
+ node_info = NODE_TYPES.get(node.node_data.node_type, {})
2610
+ self.type_display.setText(node_info.get('label', node.node_data.node_type))
2611
+
2612
+ self.desc_input.blockSignals(True)
2613
+ self.desc_input.setPlainText(node.node_data.description)
2614
+ self.desc_input.blockSignals(False)
2615
+
2616
+ # Code status for source nodes
2617
+ if node.node_data.node_type == 'source':
2618
+ self.code_section.show()
2619
+ py_path = node.node_data.generated_files.get('python', '')
2620
+ plugin_path = node.node_data.generated_files.get('plugin', '')
2621
+
2622
+ if py_path:
2623
+ self.python_status.setText(f"Python: {Path(py_path).name}")
2624
+ self.python_status.setStyleSheet(f'color: {THEME["accent_green"]};')
2625
+ else:
2626
+ self.python_status.setText("Python: Not created")
2627
+ self.python_status.setStyleSheet(f'color: {THEME["text_secondary"]};')
2628
+
2629
+ if plugin_path:
2630
+ self.plugin_status.setText(f"Plugin: {Path(plugin_path).name}")
2631
+ self.plugin_status.setStyleSheet(f'color: {THEME["accent_green"]};')
2632
+ else:
2633
+ self.plugin_status.setText("Plugin: Not created")
2634
+ self.plugin_status.setStyleSheet(f'color: {THEME["text_secondary"]};')
2635
+ else:
2636
+ self.code_section.hide()
2637
+
2638
+ def _on_name_changed(self, text):
2639
+ if self.current_node and text:
2640
+ self.current_node.update_name(text)
2641
+
2642
+ def _on_desc_changed(self):
2643
+ if self.current_node:
2644
+ self.current_node.update_description(self.desc_input.toPlainText())
2645
+
2107
2646
 
2108
2647
  # ========================================================================
2109
2648
  # File Tree Panel
@@ -2486,6 +3025,82 @@ if PYQT_AVAILABLE:
2486
3025
  paste_btn.clicked.connect(lambda: self.canvas.paste_nodes() if hasattr(self, 'canvas') else None)
2487
3026
  layout.addWidget(paste_btn)
2488
3027
 
3028
+ layout.addWidget(self._create_separator())
3029
+
3030
+ # Layout/Arrange buttons
3031
+ align_h_btn = QPushButton("Align H")
3032
+ align_h_btn.setStyleSheet(btn_style)
3033
+ align_h_btn.setToolTip("Align selected nodes horizontally")
3034
+ align_h_btn.clicked.connect(lambda: self.canvas.align_horizontal() if hasattr(self, 'canvas') else None)
3035
+ layout.addWidget(align_h_btn)
3036
+
3037
+ align_v_btn = QPushButton("Align V")
3038
+ align_v_btn.setStyleSheet(btn_style)
3039
+ align_v_btn.setToolTip("Align selected nodes vertically")
3040
+ align_v_btn.clicked.connect(lambda: self.canvas.align_vertical() if hasattr(self, 'canvas') else None)
3041
+ layout.addWidget(align_v_btn)
3042
+
3043
+ arrange_btn = QPushButton("Auto-Arrange")
3044
+ arrange_btn.setStyleSheet(btn_style)
3045
+ arrange_btn.setToolTip("Automatically arrange all nodes")
3046
+ arrange_btn.clicked.connect(lambda: self.canvas.auto_arrange() if hasattr(self, 'canvas') else None)
3047
+ layout.addWidget(arrange_btn)
3048
+
3049
+ layout.addWidget(self._create_separator())
3050
+
3051
+ # Quick-add buttons with accent colors
3052
+ source_btn = QPushButton("+ Source")
3053
+ source_btn.setStyleSheet(f'''
3054
+ QPushButton {{
3055
+ background: {THEME['accent_green']};
3056
+ border: none;
3057
+ border-radius: 6px;
3058
+ color: white;
3059
+ padding: 6px 12px;
3060
+ font-size: 11px;
3061
+ font-weight: bold;
3062
+ }}
3063
+ QPushButton:hover {{
3064
+ background: #00ff88;
3065
+ }}
3066
+ ''')
3067
+ source_btn.clicked.connect(lambda: self._quick_add_node('source'))
3068
+ layout.addWidget(source_btn)
3069
+
3070
+ class_btn = QPushButton("+ Class")
3071
+ class_btn.setStyleSheet(f'''
3072
+ QPushButton {{
3073
+ background: {THEME['accent_blue']};
3074
+ border: none;
3075
+ border-radius: 6px;
3076
+ color: white;
3077
+ padding: 6px 12px;
3078
+ font-size: 11px;
3079
+ }}
3080
+ QPushButton:hover {{
3081
+ background: #5abeff;
3082
+ }}
3083
+ ''')
3084
+ class_btn.clicked.connect(lambda: self._quick_add_node('class'))
3085
+ layout.addWidget(class_btn)
3086
+
3087
+ func_btn = QPushButton("+ Function")
3088
+ func_btn.setStyleSheet(f'''
3089
+ QPushButton {{
3090
+ background: #50c878;
3091
+ border: none;
3092
+ border-radius: 6px;
3093
+ color: white;
3094
+ padding: 6px 12px;
3095
+ font-size: 11px;
3096
+ }}
3097
+ QPushButton:hover {{
3098
+ background: #60d888;
3099
+ }}
3100
+ ''')
3101
+ func_btn.clicked.connect(lambda: self._quick_add_node('function'))
3102
+ layout.addWidget(func_btn)
3103
+
2489
3104
  layout.addStretch()
2490
3105
 
2491
3106
  self.search_input = QLineEdit()
@@ -2635,10 +3250,29 @@ if PYQT_AVAILABLE:
2635
3250
  self.canvas.status_message.connect(
2636
3251
  lambda msg: self.status_label.setText(msg)
2637
3252
  )
3253
+ # Wire selection changes to properties panel
3254
+ self.canvas.scene.selectionChanged.connect(self._on_selection_changed)
2638
3255
  codemaker_layout.addWidget(self.canvas, 1)
2639
3256
 
3257
+ # Properties panel on the right
3258
+ self.properties_panel = PropertiesPanel()
3259
+ codemaker_layout.addWidget(self.properties_panel)
3260
+
2640
3261
  self.main_stack_layout.addWidget(codemaker)
2641
3262
 
3263
+ def _on_selection_changed(self):
3264
+ """Handle selection change in canvas."""
3265
+ if not hasattr(self, 'canvas') or not hasattr(self, 'properties_panel'):
3266
+ return
3267
+
3268
+ selected = [item for item in self.canvas.scene.selectedItems()
3269
+ if isinstance(item, VisualNode)]
3270
+
3271
+ if len(selected) == 1:
3272
+ self.properties_panel.update_selection(selected[0])
3273
+ else:
3274
+ self.properties_panel.update_selection(None)
3275
+
2642
3276
  def _load_map(self, file_path: str):
2643
3277
  """Load a map file"""
2644
3278
  try:
@@ -2687,6 +3321,22 @@ if PYQT_AVAILABLE:
2687
3321
  def mouseReleaseEvent(self, event):
2688
3322
  self._drag_pos = None
2689
3323
 
3324
+ def _quick_add_node(self, node_type: str):
3325
+ """Quick-add a node from toolbar button."""
3326
+ if not hasattr(self, 'canvas'):
3327
+ return
3328
+
3329
+ name, ok = QInputDialog.getText(
3330
+ self, f"New {NODE_TYPES[node_type]['label']}",
3331
+ "Name:"
3332
+ )
3333
+ if ok and name:
3334
+ # Add at center of current view
3335
+ center = self.canvas.mapToScene(
3336
+ self.canvas.viewport().rect().center()
3337
+ )
3338
+ self.canvas.add_node(node_type, name, center.x(), center.y())
3339
+
2690
3340
  def closeEvent(self, event):
2691
3341
  self._save_current()
2692
3342
  super().closeEvent(event)