From 66f16d1771e4fcd49914e6dc60dc6f159fba20a4 Mon Sep 17 00:00:00 2001 From: tyler Date: Fri, 15 May 2026 02:51:19 -0700 Subject: [PATCH] fun with blocks --- blocks.json | 43 +++-- main.py | 505 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 452 insertions(+), 96 deletions(-) diff --git a/blocks.json b/blocks.json index d2b3845..6aedf66 100644 --- a/blocks.json +++ b/blocks.json @@ -1,6 +1,6 @@ { "individual": [ - { "name": "New Event", "type": "begin" }, + { "name": "New Event", "type": "begin", "sub_type": "created_event" }, { "name": "Average", "type": "middle", @@ -37,24 +37,29 @@ { "name": "End", "type": "end" }, - { "name": "Joint 1", "type": "logic" }, - { "name": "Joint 2", "type": "logic" }, - { "name": "Joint 3", "type": "logic" }, - { "name": "Joint 4", "type": "logic" }, - { "name": "Joint 5", "type": "logic" }, - { "name": "Joint 6", "type": "logic" }, - { "name": "Joint 7", "type": "logic" }, - { "name": "Joint 8", "type": "logic" }, - { "name": "Joint 9", "type": "logic" }, - { "name": "Joint 10", "type": "logic" }, - { "name": "Joint 11", "type": "logic" }, - { "name": "Joint 12", "type": "logic" }, - { "name": "Joint 13", "type": "logic" }, - { "name": "Joint 14", "type": "logic" }, - { "name": "Joint 15", "type": "logic" }, - { "name": "Joint 16", "type": "logic" }, - { "name": "Joint 17", "type": "logic" }, - { "name": "New Calculated Joint", "type": "begin" } + { "name": "Nose", "type": "logic" }, + { "name": "Left Eye", "type": "logic" }, + { "name": "Right Eye", "type": "logic" }, + { "name": "Left Ear", "type": "logic" }, + { "name": "Right Ear", "type": "logic" }, + { "name": "Left Shoulder", "type": "logic" }, + { "name": "Right Shoulder", "type": "logic" }, + { "name": "Left Elbow", "type": "logic" }, + { "name": "Right Elbow", "type": "logic" }, + { "name": "Left Wrist", "type": "logic" }, + { "name": "Right Wrist", "type": "logic" }, + { "name": "Left Hip", "type": "logic" }, + { "name": "Right Hip", "type": "logic" }, + { "name": "Left Knee", "type": "logic" }, + { "name": "Right Knee", "type": "logic" }, + { "name": "Left Ankle", "type": "logic" }, + { "name": "Right Ankle", "type": "logic" }, + { "name": "New Calculated Joint", "type": "begin", "sub_type": "created_joint" } + ], + + "group": [ + { "name": "New Activity", "type": "begin" }, + { "name": "End Activity", "type": "end" } ] } \ No newline at end of file diff --git a/main.py b/main.py index d1e923c..92fda92 100644 --- a/main.py +++ b/main.py @@ -34,7 +34,7 @@ from pose_worker import run_pose_analysis from batch_processing import BatchProcessorDialog import PySide6 -from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QFormLayout, QGraphicsItem, QGraphicsProxyWidget, QGraphicsTextItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QSizePolicy, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, +from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QFormLayout, QGraphicsItem, QGraphicsProxyWidget, QGraphicsTextItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QSizePolicy, QStyleOptionGraphicsItem, QTabBar, QTreeWidget, QTreeWidgetItem, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog, QGraphicsRectItem, QFileDialog, QScrollArea, QMessageBox, QSlider, QTextEdit, QGroupBox, QGridLayout, QCheckBox, QTabWidget, QProgressBar) from PySide6.QtCore import QEvent, QObject, Qt, QThread, Signal, QUrl, QRectF, QPointF, QRect, QSizeF, QTimer @@ -3165,10 +3165,10 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsSimpleTextItem, QInputDial class BlockSignalProxy(QObject): nameChanged = Signal(str, str) + class PuzzleBlock(QGraphicsPathItem): - - def __init__(self, b_type, label, parent_item=None, fields=None): + def __init__(self, b_type, label, parent_item=None, fields=None, sub_type="standard"): super().__init__(parent_item) self.signals = BlockSignalProxy() @@ -3178,6 +3178,8 @@ class PuzzleBlock(QGraphicsPathItem): self.width = 160 self.height = 50 self.tab_size = 15 + self.fields = fields if fields is not None else [] + self.sub_type = sub_type self.parent_block = None self.child_block = None @@ -3404,15 +3406,38 @@ class PuzzleBlock(QGraphicsPathItem): self.signals.nameChanged.emit(old_text, new_text) -class BlockLibrary(QListWidget): + def force_resize(self): + """Recalculates height based on layout and updates the visual path.""" + if hasattr(self, 'container') and self.container.layout(): + # Force the layout to settle + self.container.layout().activate() + # Update height based on the new size hint + self.height = 50 + self.container.layout().sizeHint().height() + self.update_path() + + +class BlockLibrary(QTreeWidget): def __init__(self, parent_tab, stringy): super().__init__() self.setDragEnabled(True) + self.setHeaderHidden(True) # Hide the "1" header at the top + self.setIndentation(15) + self.setAnimated(True) self.mode = stringy self.parent_tab = parent_tab # Reference to main tab to access canvas + self.categories = {} + self.type_themes = { + "begin": {"label": "ENTRY EVENTS", "color": "#2e7d32"}, # Dark Green + "middle": {"label": "PROCESSORS", "color": "#1565c0"}, # Dark Blue + "end": {"label": "EXPORTS", "color": "#c62828"}, # Dark Red + "logic": {"label": "VARIABLES", "color": "#ef6c00"}, # Dark Orange + "created_event": {"label": "CREATED EVENTS", "color": "#6a1b9a"}, # Deep Purple + "created_joint": {"label": "CALCULATED JOINTS", "color": "#00695c"} # Teal/Blue-Green + } + self.load_blocks_from_json("blocks.json") @@ -3430,35 +3455,84 @@ class BlockLibrary(QListWidget): for block in blocks: # NEW: Get fields if they exist, otherwise empty list - fields = block.get('fields', []) - self.add_item(block['name'], block['type'], fields) + self.add_item(block['name'], block['type'], block.get('fields', []), block.get('sub_type', "standard")) except Exception as e: print(f"Failed to load block library: {e}") + + def import_individual_events(self): + individual_config = "config_individual.json" + if not os.path.exists(individual_config): return - def add_item(self, label, block_type, fields): - item = QListWidgetItem(label) - item.setData(Qt.UserRole, block_type) - # NEW: Store fields list in the item - item.setData(Qt.UserRole + 1, fields) - self.addItem(item) + try: + with open(individual_config, 'r') as f: + save_data = json.load(f) + + for block in save_data: + # Check for the specific sub_type we just defined + # We specifically want 'begin' blocks that were marked as events + if block.get('b_type') == "begin": + # Use the custom name from the inputs if available + inputs = block.get('inputs', {}) + # Assuming your "New Event" block has a QLineEdit for the name + event_name = next(iter(inputs.values()), block.get('label')) + + print(f"[DEBUG] Importing: {event_name}") + self.add_item(event_name, "created_event", fields=[], sub_type="standard") + + except Exception as e: + print(f"Error importing individual events: {e}") + + + def get_or_create_category(self, b_type): + """Helper to ensure category dropdowns exist and are styled.""" + if b_type not in self.categories: + theme = self.type_themes.get(b_type, {"label": b_type.upper(), "color": "#444444"}) + + # Create top-level category item + cat = QTreeWidgetItem(self) + cat.setText(0, theme["label"]) + cat.setBackground(0, QBrush(QColor(theme["color"]))) + cat.setForeground(0, QBrush(QColor("white"))) + cat.setFlags(cat.flags() & ~Qt.ItemIsDragEnabled) # Categories can't be dragged + + self.categories[b_type] = cat + cat.setExpanded(True) # Start with dropdowns open + + return self.categories[b_type] + + def add_item(self, label, block_type, fields, sub_type): + category = self.get_or_create_category(block_type) + + # Create child item under the category + item = QTreeWidgetItem(category) + item.setText(0, label) + + # Store metadata + item.setData(0, Qt.UserRole, block_type) + item.setData(0, Qt.UserRole + 1, fields) + item.setData(0, Qt.UserRole + 2, sub_type) + + # Optional: Subtle styling for child items + item.setForeground(0, QBrush(QColor("#dddddd"))) def startDrag(self, supportedActions): item = self.currentItem() - if not item: return + if not item or item.parent() is None: + return + + # Pack everything into a clean dictionary + drag_data = { + "type": item.data(0, Qt.UserRole), + "label": item.text(0), + "fields": item.data(0, Qt.UserRole + 1), + "sub_type": item.data(0, Qt.UserRole + 2) # Use the type as subtype by default + } mime_data = QMimeData() - - # NEW: We must include the fields in the drag data! - # Format: type | label | fields_as_json_string - b_type = item.data(Qt.UserRole) - label = item.text() - fields = item.data(Qt.UserRole + 1) - fields_str = json.dumps(fields) # Convert list to string for transfer - - mime_data.setText(f"{b_type}|{label}|{fields_str}") + mime_data.setText(json.dumps(drag_data)) # Send as ONE structured string drag = QDrag(self) drag.setMimeData(mime_data) @@ -3468,38 +3542,141 @@ class BlockLibrary(QListWidget): def add_dynamic_logic_item(self, block_item): + print("asdasd") """ - Triggered when a 'begin' block is dropped on the canvas. - Adds a corresponding 'logic' block to this library. + Triggered immediately when a 'begin' block is dropped on the canvas. """ - # 1. Check for duplicates - # We don't want to add "Joint 1" five times if they drag five 'begin' blocks - + # 1. Get the actual identity we assigned in dropEvent + # This will now be 'created_joint' or 'created_event' instead of just 'begin' + s_type = getattr(block_item, 'sub_type', 'standard') label = block_item.label_text - items = self.findItems(label, Qt.MatchExactly) - for item in items: - if item.data(Qt.UserRole) == "logic": - print(f"DEBUG: {label} (logic) already exists in library. Skipping.") + # 2. Route to the correct sidebar category + if s_type == "created_joint": + target_category = "created_joint" # Teal + elif s_type == "created_event": + target_category = "created_event" # Orange (Individual Variables) + else: + return # Don't add standard 'begin' blocks to the logic list + + # 3. Prevent Duplicate Sidebar Entries + category_item = self.get_or_create_category(target_category) + for i in range(category_item.childCount()): + if category_item.child(i).text(0) == label: return - # 2. Add the item as a 'logic' type - # In your system, logic blocks have no fields (they are just variables) - print(f"DEBUG: Dynamically adding '{label}' as a logic block.") - self.add_item(label, "logic", fields=[]) + # 4. Update the Sidebar UI immediately + self.add_item(label, target_category, fields=[], sub_type=s_type) + print(f"[DEBUG] Instant Sidebar Update: Added {label} to {target_category}") + def update_item_name(self, old_name, new_name): - print(f"[DEBUG - Library] Searching for items matching: '{old_name}'") - items = self.findItems(old_name, Qt.MatchExactly) - - if not items: - print(f"[DEBUG - Library] No items found matching '{old_name}'") + """Rename logic blocks in the VARIABLES dropdown.""" + cat = self.get_or_create_category("created_joint") + for i in range(cat.childCount()): + item = cat.child(i) + if item.text(0) == old_name: + item.setText(0, new_name) + + + def discover_saved_blocks(self, save_file, target_subtype): + """Scans save file and populates categories using the helper function.""" + if not os.path.exists(save_file): + return + + try: + with open(save_file, 'r') as f: + data = json.load(f) - for item in items: - # Ensure we only rename the logic-type items - if item.data(Qt.UserRole) == "logic": - item.setText(new_name) - print(f"[DEBUG - Library] Successfully updated sidebar item to '{new_name}'") + # Use the helper to ensure the Teal or Purple folder exists + target_cat = self.get_or_create_category(target_subtype) + + if not target_cat: + print(f"[ERROR] Could not resolve category for: {target_subtype}") + return + + for block_data in data: + if block_data.get('sub_type') == target_subtype: + # Resolve name (LineEdit input or generic label) + inputs = block_data.get('inputs', {}) + name = next(iter(inputs.values()), block_data.get('label')) + + # Scoped duplicate check + is_duplicate = False + for i in range(target_cat.childCount()): + if target_cat.child(i).text(0) == name: + is_duplicate = True + break + + if not is_duplicate: + self.add_item( + label=name, + block_type=target_subtype, + fields=[], + sub_type=target_subtype + ) + print(f"[DEBUG] Restored '{name}' to {target_cat.text(0)}") + + except Exception as e: + print(f"[ERROR] Discovery error: {e}") + + + def remove_dynamic_logic_item(self, label, s_type): + """ + Surgical removal from the sidebar. + """ + target_label = str(label).strip().lower() + + # BLACKLIST: Never search or delete from these category keys. + # These represent your "Factory" templates and "Nose/Eye" entries. + forbidden_categories = ["begin", "entry_events", "standard_nodes"] + + # WHITELIST: Only look for the spawns/variables here. + allowed_categories = ["created_joint", "logic", "created_event"] + + for cat_key in allowed_categories: + if cat_key in self.categories and cat_key not in forbidden_categories: + cat = self.categories[cat_key] + for i in range(cat.childCount() - 1, -1, -1): + child = cat_item = cat.child(i) + if child.text(0).strip().lower() == target_label: + cat.removeChild(child) + print(f"[DEBUG] Sidebar: Removed '{label}' from {cat_key}") + return # Exit once the spawn is deleted + + # def add_dynamic_logic_item(self, block_item): + # """ + # Triggered when a 'begin' block is dropped on the canvas. + # Adds a corresponding 'logic' block to this library. + # """ + # # 1. Check for duplicates + # # We don't want to add "Joint 1" five times if they drag five 'begin' blocks + + # label = block_item.label_text + + # items = self.findItems(label, Qt.MatchExactly) + # for item in items: + # if item.data(Qt.UserRole) == "logic": + # print(f"DEBUG: {label} (logic) already exists in library. Skipping.") + # return + + # # 2. Add the item as a 'logic' type + # # In your system, logic blocks have no fields (they are just variables) + # print(f"DEBUG: Dynamically adding '{label}' as a logic block.") + # self.add_item(label, "logic", fields=[]) + + # def update_item_name(self, old_name, new_name): + # print(f"[DEBUG - Library] Searching for items matching: '{old_name}'") + # items = self.findItems(old_name, Qt.MatchExactly) + + # if not items: + # print(f"[DEBUG - Library] No items found matching '{old_name}'") + + # for item in items: + # # Ensure we only rename the logic-type items + # if item.data(Qt.UserRole) == "logic": + # item.setText(new_name) + # print(f"[DEBUG - Library] Successfully updated sidebar item to '{new_name}'") from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem @@ -3509,6 +3686,7 @@ from PySide6.QtGui import QPen, QColor, QBrush class TopologyCanvas(QGraphicsView): beginBlockDropped = Signal(object) + beginBlockDeleted = Signal(str, str) def __init__(self): self.scene = QGraphicsScene() @@ -3521,7 +3699,7 @@ class TopologyCanvas(QGraphicsView): def on_new_definition_created(self, block_item): # Connect the block's rename signal to our canvas sync method print(f"[DEBUG - Canvas] Connecting signal for new block: {block_item.label_text}") - block_item.signals.nameChanged.connect(self.sync_logic_blocks) + self.wire_block_signals(block_item) def sync_logic_blocks(self, old_name, new_name): print(f"[DEBUG - Canvas] Syncing. Old: {old_name}, New: {new_name}") @@ -3569,6 +3747,7 @@ class TopologyCanvas(QGraphicsView): # Defer the logic to the next event loop cycle (stops the DLL crash) QTimer.singleShot(0, lambda: self.safe_process_release(global_pos, scene_pos)) + def safe_process_release(self, global_pos, scene_pos): self._processing_release = True try: @@ -3602,33 +3781,45 @@ class TopologyCanvas(QGraphicsView): def dropEvent(self, event): - raw_data = event.mimeData().text() - parts = raw_data.split("|") - if len(parts) < 2: return # Safety check - - block_type, label = parts[0], parts[1] - fields = [] + try: + # 1. Parse the structured JSON + raw_data = event.mimeData().text() + data = json.loads(raw_data) + + # 2. Extract with safe defaults + b_type = data.get("type", "middle") + label = data.get("label", "New Block") + fields = data.get("fields", []) + sub_type = data.get("sub_type", "standard") - # Safe JSON parsing - if len(parts) > 2: - try: - fields = json.loads(parts[2]) - except Exception as e: - print(f"DEBUG: Failed to parse: {e}") + print(sub_type) - - new_block = PuzzleBlock(block_type, label, fields=fields) + # 3. Determine the visual class of the block + # (Same logic as before, but cleaner) + if b_type == "created_event": + visual_type = "begin" + elif b_type == "created_joint": + visual_type = "logic" + else: + visual_type = b_type - self.scene.addItem(new_block) - - # Try snapping, otherwise place at drop location - if not self.perform_snap(new_block, self.mapToScene(event.position().toPoint())): - new_block.setPos(self.mapToScene(event.position().toPoint())) - - if block_type == "begin": - self.beginBlockDropped.emit(new_block) + # 4. Create the block + new_block = PuzzleBlock(visual_type, label, fields=fields, sub_type=sub_type) + self.scene.addItem(new_block) + + # 5. Position + drop_pos = self.mapToScene(event.position().toPoint()) + if not self.perform_snap(new_block, drop_pos): + new_block.setPos(drop_pos) + + # 6. Library Feedback + if visual_type == "begin": + self.beginBlockDropped.emit(new_block) - event.acceptProposedAction() + event.acceptProposedAction() + + except Exception as e: + print(f"CRITICAL: Failed to parse drag-and-drop JSON: {e}") def get_stack_extremity(self, block, direction): @@ -3644,14 +3835,6 @@ class TopologyCanvas(QGraphicsView): return curr - def keyPressEvent(self, event): - if event.key() == Qt.Key.Key_F7: - self.diagnostic_dump() - if event.key() == Qt.Key.Key_F8: - self.print_logical_graphs() - super().keyPressEvent(event) - - def print_logical_graphs(self): """Crawls the logical links and prints all current stacks.""" print(f"\n{'='*60}\nCURRENT LOGICAL GRAPHS (F8 Triggered)") @@ -3957,7 +4140,168 @@ class TopologyCanvas(QGraphicsView): + def save_to_json(self, file_path="config.json"): + data = [] + blocks = [item for item in self.scene.items() if isinstance(item, PuzzleBlock)] + + # Mapping to store IDs for linking + for block in blocks: + block_data = { + "id": id(block), + "b_type": block.b_type, + "sub_type": getattr(block, 'sub_type', 'standard'), + "label": block.label_text, + "pos": [block.pos().x(), block.pos().y()], + "parent_id": id(block.parent_block) if block.parent_block else None, + "child_id": id(block.child_block) if block.child_block else None, + "host_id": id(block.host_block) if getattr(block, 'host_block', None) else None, + "slot_key": getattr(block, 'current_slot', None), + "fields": block.fields, + "inputs": {k: v.text() for k, v in block.inputs.items() if isinstance(v, QLineEdit)} + } + data.append(block_data) + with open(file_path, 'w') as f: + json.dump(data, f, indent=4) + print(f"Configuration saved to {file_path}") + + + def load_from_json(self, file_path="config.json"): + if not os.path.exists(file_path): + return + + self.scene.clear() + with open(file_path, 'r') as f: + data = json.load(f) + + id_map = {} + + # 1. Create all blocks + for b in data: + # Note: Pass the original fields if you have them, + # or ensure PuzzleBlock handles empty fields gracefully + new_block = PuzzleBlock(b['b_type'], b['label'], fields=b['fields'], sub_type=b.get('sub_type', 'standard')) + new_block.setPos(b['pos'][0], b['pos'][1]) + + # Restore text inputs + for key, val in b.get('inputs', {}).items(): + if key in new_block.inputs: + widget = new_block.inputs[key] + if isinstance(widget, QLineEdit): + widget.setText(val) + + # CRITICAL: Force the block to wrap around its new content + new_block.force_resize() + + self.scene.addItem(new_block) + id_map[b['id']] = new_block + self.wire_block_signals(new_block) + + # 2. Re-establish logical links + for b in data: + curr = id_map[b['id']] + + # Connect Chained Stacks (Vertical) + if b.get('child_id') in id_map: + curr.child_block = id_map[b['child_id']] + if b.get('parent_id') in id_map: + curr.parent_block = id_map[b['parent_id']] + + # Connect Embedded Logic Blocks (Horizontal/Internal) + if b.get('host_id') in id_map: + host = id_map[b['host_id']] + slot_key = b['slot_key'] + # Re-running embed_logic_block will fix the internal Z-value and position + self.embed_logic_block(curr, host, slot_key) + + if hasattr(self, 'block_library_ref') and self.block_library_ref: + # Determine what we are looking for based on current editor mode + target = "created_joint" if self.block_library_ref.mode == "individual" else "created_event" + self.block_library_ref.discover_saved_blocks(file_path, target) + + # 3. Final visual refresh + self.scene.update() + + + def keyPressEvent(self, event): + if event.key() == Qt.Key.Key_F7: + self.diagnostic_dump() + if event.key() == Qt.Key.Key_F8: + self.print_logical_graphs() + if event.key() in (Qt.Key_Delete, Qt.Key_Backspace): + # Get all selected items on the canvas + selected_items = self.scene.selectedItems() + + for item in selected_items: + if isinstance(item, PuzzleBlock): + self.delete_block(item) + else: + # Crucial: let other keys (like arrows for moving blocks) work normally + super().keyPressEvent(event) + + + def delete_block(self, block): + """ + Surgical Deletion with Orphan Cleanup: + 1. Removes master definition and signals sidebar. + 2. Scans canvas for any 'logic' instances of this block and deletes them. + """ + if not block: + return + + # 1. IDENTIFY IDENTITY + v_type = getattr(block, 'b_type', 'middle') + s_type = getattr(block, 'sub_type', 'standard') + label = block.label_text + + # 2. TRIGGER SIDEBAR & ORPHAN CLEANUP + # Only if it's a master definition (Begin) of a dynamic type + if v_type == "begin" and s_type in ["created_joint", "created_event"]: + print(f"[DEBUG] Master Deleted: {label}. Cleaning orphans...") + + # Signal sidebar removal + self.beginBlockDeleted.emit(label, s_type) + + # CLEANUP ORPHANS: Find every logic block on the canvas with the same name + for item in self.scene.items(): + # We look for blocks that are NOT the one we are currently deleting + # and match the name and the 'logic' type. + if item != block and hasattr(item, 'b_type') and item.b_type == "logic": + if item.label_text == label: + print(f"[DEBUG] Removing orphaned instance: {label}") + # We call scene.removeItem directly to avoid recursive signal loops + self.scene.removeItem(item) + + # 3. HANDLE VERTICAL CONNECTIONS (STREAMS) + if hasattr(block, 'child_block') and block.child_block: + block.child_block.parent_block = None + if hasattr(block, 'parent_block') and block.parent_block: + block.parent_block.child_block = None + + # 4. HANDLE SLOTS (Embedded logic blocks) + if hasattr(block, 'slots'): + for slot_key, embedded_item in list(block.slots.items()): + if embedded_item: + self.delete_block(embedded_item) + block.slots[slot_key] = None + + # 5. PHYSICAL REMOVAL OF THE TARGET BLOCK + if block in self.scene.items(): + self.scene.removeItem(block) + + + + def wire_block_signals(self, block_item): + """Ensures a block's internal changes are reflected in the UI/Sidebar.""" + if hasattr(block_item, 'signals') and hasattr(block_item.signals, 'nameChanged'): + # Disconnect first to prevent double-triggering if called twice + try: + block_item.signals.nameChanged.disconnect(self.sync_logic_blocks) + except: + pass + + block_item.signals.nameChanged.connect(self.sync_logic_blocks) + print(f"[DEBUG - CANVAS] Wired signals for: {block_item.label_text}") class ModelParameterConfigurationTab(QWidget): @@ -4023,6 +4367,7 @@ class ModelParameterConfigurationTab(QWidget): self.setup_ui() + QTimer.singleShot(0, lambda: self.canvas.load_from_json(f"config_{self.mode}.json")) def setup_ui(self): @@ -4044,6 +4389,7 @@ class ModelParameterConfigurationTab(QWidget): self.canvas.block_library_ref = self.block_library self.canvas.beginBlockDropped.connect(self.block_library.add_dynamic_logic_item) + self.canvas.beginBlockDeleted.connect(self.block_library.remove_dynamic_logic_item) # Reuse your existing Inspector logic self.inspector_scroll = QScrollArea() @@ -4058,6 +4404,11 @@ class ModelParameterConfigurationTab(QWidget): sidebar_layout.addWidget(QLabel("Properties")) sidebar_layout.addWidget(self.inspector_scroll, 1) + self.save_btn = QPushButton("Save Configuration") + # Save specifically for this 'mode' so different tabs don't overwrite each other + self.save_btn.clicked.connect(lambda: self.canvas.save_to_json(f"config_{self.mode}.json")) + sidebar_layout.addWidget(self.save_btn) + # Add to main splitter self.horizontal_splitter.addWidget(self.canvas) self.horizontal_splitter.addWidget(sidebar_container)