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.
- includecpp/__init__.py +59 -58
- includecpp/cli/commands.py +400 -21
- includecpp/core/cppy_converter.py +143 -18
- includecpp/core/cssl/__init__.py +40 -0
- includecpp/core/cssl/cssl_builtins.py +1693 -0
- includecpp/core/cssl/cssl_events.py +621 -0
- includecpp/core/cssl/cssl_modules.py +2803 -0
- includecpp/core/cssl/cssl_parser.py +1791 -0
- includecpp/core/cssl/cssl_runtime.py +1587 -0
- includecpp/core/cssl/cssl_syntax.py +488 -0
- includecpp/core/cssl/cssl_types.py +438 -0
- includecpp/core/cssl_bridge.py +409 -0
- includecpp/core/cssl_bridge.pyi +311 -0
- includecpp/core/project_ui.py +684 -34
- includecpp/generator/parser.cpp +81 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.8.dist-info}/METADATA +48 -4
- includecpp-3.4.8.dist-info/RECORD +41 -0
- includecpp-3.3.20.dist-info/RECORD +0 -31
- {includecpp-3.3.20.dist-info → includecpp-3.4.8.dist-info}/WHEEL +0 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.8.dist-info}/entry_points.txt +0 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.8.dist-info}/licenses/LICENSE +0 -0
- {includecpp-3.3.20.dist-info → includecpp-3.4.8.dist-info}/top_level.txt +0 -0
includecpp/core/project_ui.py
CHANGED
|
@@ -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[:
|
|
588
|
-
if len(self.node_data.description) >
|
|
603
|
+
desc_text = self.node_data.description[:120]
|
|
604
|
+
if len(self.node_data.description) > 120:
|
|
589
605
|
desc_text += "..."
|
|
590
|
-
self.desc_label
|
|
591
|
-
self.
|
|
592
|
-
|
|
593
|
-
self.
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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[:
|
|
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
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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 ==
|
|
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
|
|
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)
|