From 9c8bd8a91d01ecf7b250a7a03a1270a1a40f4b8d Mon Sep 17 00:00:00 2001 From: Niko Rios Date: Wed, 17 Sep 2025 12:08:42 -0500 Subject: [PATCH 1/4] Fixed font sizing and readded suffix dropdown to format editor --- ocvl/function/gui/constructors.py | 194 +++++++++++++++++-------- ocvl/function/gui/import_generation.py | 61 ++++---- ocvl/function/gui/wizard_creator.py | 122 +++++++++++++--- 3 files changed, 267 insertions(+), 110 deletions(-) diff --git a/ocvl/function/gui/constructors.py b/ocvl/function/gui/constructors.py index dd548b0..545756a 100644 --- a/ocvl/function/gui/constructors.py +++ b/ocvl/function/gui/constructors.py @@ -6,7 +6,7 @@ QLabel, QComboBox, QTextEdit, QLineEdit, QPushButton, QRadioButton, QHBoxLayout, QButtonGroup, QCheckBox, QSizePolicy, QDialog, QDialogButtonBox, QFileDialog, QSplitter, QListWidget, - QInputDialog, QMenu, QMessageBox, QFrame, QGridLayout) + QInputDialog, QMenu, QMessageBox, QFrame, QGridLayout, QListWidgetItem) from PySide6.QtCore import Qt, QSize, Signal from PySide6.QtWidgets import QWidget, QHBoxLayout, QCheckBox @@ -323,25 +323,34 @@ class FormatElementsEditor(QDialog): """Dialog for editing filename format using predefined elements""" copyRequested = Signal(str, str, str) - def __init__(self, current_format=None, parent=None, queryloc=False, section_name=None, format_key=None, enable_copy=True): + def __init__(self, current_format=None, parent=None, type=None, section_name=None, format_key=None, + enable_copy=True): super().__init__(parent) self.setWindowTitle("Format Editor") - self.setGeometry(500, 500, 650, 500) + self.setGeometry(600, 600, 650, 500) self.original_elements = [ ":.1", "Day", "Eye", "FOV_Height", "FOV_Width", "IDnum", "LocX", "LocY", "Modality", "Month", "VidNum", "Year" ] - if queryloc: self.original_elements.append("QueryLoc") + if type == 'queryloc': + self.original_elements.append("QueryLoc") self.available_elements = self.original_elements.copy() self.section_name = section_name self.format_key = format_key + self.type = type + + self.extension_options = self._get_extension_options() self.copy_button = QPushButton("Copy to All in Section") self.copy_button.clicked.connect(self.copy_to_all) + self.file_type_combo = QComboBox() + self.file_type_combo.addItems(self.extension_options) + self.file_type_combo.currentTextChanged.connect(self.update_preview) + # === MAIN VERTICAL LAYOUT === window_layout = QVBoxLayout(self) @@ -351,15 +360,19 @@ def __init__(self, current_format=None, parent=None, queryloc=False, section_nam self.preview_label.setStyleSheet("font-weight: bold; padding: 6px;") self.preview_display = QLabel("") self.preview_display.setStyleSheet(""" - QLabel { - padding: 6px; - color: #333; - min-height: 40px; + QToolButton { + border: none; + text-align: left; + padding: 5px; + } + QToolButton:disabled { + color: gray; } """) preview_layout.addWidget(self.preview_label) preview_layout.addWidget(self.preview_display) + preview_layout.addWidget(self.file_type_combo) if enable_copy: preview_layout.addWidget(self.copy_button) preview_layout.setAlignment(Qt.AlignLeft) @@ -438,7 +451,7 @@ def __init__(self, current_format=None, parent=None, queryloc=False, section_nam self.move_up_button = QPushButton("Move Up") self.move_down_button = QPushButton("Move Down") - self.add_separator_button = QPushButton("Add Static Text") + self.add_separator_button = QPushButton("Add Text") self.clear_button = QPushButton("Clear All") # Make buttons consistent size @@ -510,6 +523,32 @@ def __init__(self, current_format=None, parent=None, queryloc=False, section_nam # Update the preview initially self.update_preview() + def _add_item_to_selected_list(self, internal_text, display_text=None): + """Helper method to add items to selected list with internal and display text""" + item = QListWidgetItem() + item.setData(Qt.UserRole, internal_text) # Store internal representation + item.setText(display_text if display_text else internal_text) # Set display text + self.selected_list.addItem(item) + return item + + def _get_item_internal_text(self, item): + """Get the internal text representation of an item""" + internal_text = item.data(Qt.UserRole) + return internal_text if internal_text else item.text() + + def _get_extension_options(self): + """Get file extension options based on format type""" + if self.type == "image": + return [".tif", ".png", ".jpg", ".mat", ".npy"] + elif self.type == "video" or "mask": + return [".avi", ".mov", ".mat", ".npy"] + elif self.type == "meta": + return [".txt", ".json", ".xml", ".csv", ".log"] + elif self.type == "queryloc": + return [".txt", ".csv", ".json", ".dat"] + else: + return [".txt", ".dat", ".log"] + def copy_to_all(self): reply = QMessageBox.question( self, @@ -529,7 +568,8 @@ def clear_all_elements(self): items_to_return = [] for i in range(self.selected_list.count()): - item_text = self.selected_list.item(i).text() + item = self.selected_list.item(i) + item_text = self._get_item_internal_text(item) # Only return format elements (not static text) to available list if item_text.startswith("{") and item_text.endswith("}") and not item_text.startswith("{Added Text:"): @@ -568,7 +608,7 @@ def set_element_width(self): "Please select an element from the Selected Format Elements list first.") return - item_text = current_item.text() + item_text = self._get_item_internal_text(current_item) # Check if it's a format element (not a separator) if not item_text.startswith("{") or not item_text.endswith("}") or item_text.startswith("{Added Text:"): @@ -606,7 +646,9 @@ def set_element_width(self): else: # Update the element with new width new_element = f"{{{element_name}:.{width}}}" - current_item.setText(new_element) + + current_item.setData(Qt.UserRole, new_element) + current_item.setText(new_element) # Display text same as internal for format elements self.update_preview() except ValueError: QMessageBox.warning(self, "Invalid Width", "Width must be a number") @@ -628,25 +670,25 @@ def parse_format(self, format_string): if start_idx == -1: # No more elements in brackets, add remaining as separator if not empty if remaining: - self.selected_list.addItem(f"{{Added Text: {remaining}}}") + self._add_item_to_selected_list(f"{{Added Text: {remaining}}}", remaining) break # Add any text before the bracket as a separator if start_idx > 0: separator = remaining[:start_idx] - self.selected_list.addItem(f"{{Added Text: {separator}}}") + self._add_item_to_selected_list(f"{{Added Text: {separator}}}", separator) # Find closing bracket end_idx = remaining.find('}', start_idx) if end_idx == -1: # No closing bracket, add the rest as a separator - self.selected_list.addItem(f"{{Added Text: {remaining}}}") + self._add_item_to_selected_list(f"{{Added Text: {remaining}}}", remaining) break # Extract the element name element = remaining[start_idx + 1:end_idx] if element in self.available_elements or element == ":.1": - self.selected_list.addItem(f"{{{element}}}") + self._add_item_to_selected_list(f"{{{element}}}") # Remove from available elements if element in self.available_elements: self.available_elements.remove(element) @@ -656,7 +698,7 @@ def parse_format(self, format_string): self.available_list.addItems(self.available_elements) else: # Not a recognized element, treat as a separator with brackets - self.selected_list.addItem(f"{{Added Text: {{{element}}}}}") + self._add_item_to_selected_list(f"{{Added Text: {{{element}}}}}", f"{{{element}}}") # Continue with the remaining string remaining = remaining[end_idx + 1:] @@ -665,7 +707,8 @@ def get_used_elements(self): """Return a set of elements already in use""" used_elements = set() for i in range(self.selected_list.count()): - item_text = self.selected_list.item(i).text() + item = self.selected_list.item(i) + item_text = self._get_item_internal_text(item) if item_text.startswith("{") and item_text.endswith("}") and not item_text.startswith("{Added Text:"): element = item_text[1:-1] # Remove the braces # Get base element name (without width specification) @@ -682,7 +725,7 @@ def add_selected_element(self): return element = self.available_list.currentItem().text() - self.selected_list.addItem(f"{{{element}}}") + self._add_item_to_selected_list(f"{{{element}}}") # Remove from available list row = self.available_list.currentRow() @@ -699,7 +742,7 @@ def remove_selected_element(self): return item = self.selected_list.currentItem() - item_text = item.text() + item_text = self._get_item_internal_text(item) # Only return to available list if it's a format element (not a separator) if item_text.startswith("{") and item_text.endswith("}") and not item_text.startswith("{Added Text:"): @@ -727,7 +770,7 @@ def remove_selected_element(self): def handle_double_click_available(self, item): """Handle double-click on available list item""" element = item.text() - self.selected_list.addItem(f"{{{element}}}") + self._add_item_to_selected_list(f"{{{element}}}") # Remove from available list row = self.available_list.row(item) @@ -738,7 +781,7 @@ def handle_double_click_available(self, item): def handle_double_click_selected(self, item): """Handle double-click on selected list item""" - item_text = item.text() + item_text = self._get_item_internal_text(item) # Only return to available list if it's a format element (not a separator) if item_text.startswith("{") and item_text.endswith("}") and not item_text.startswith("{Added Text:"): @@ -792,33 +835,36 @@ def add_separator(self): # Get selected position or append to end current_row = self.selected_list.currentRow() if current_row >= 0: - self.selected_list.insertItem(current_row + 1, f"{{Added Text: {text}}}") + item = self._add_item_to_selected_list(f"{{Added Text: {text}}}", text) + self.selected_list.insertItem(current_row + 1, + self.selected_list.takeItem(self.selected_list.count() - 1)) self.selected_list.setCurrentRow(current_row + 1) else: - self.selected_list.addItem(f"{{Added Text: {text}}}") + self._add_item_to_selected_list(f"{{Added Text: {text}}}", text) self.selected_list.setCurrentRow(self.selected_list.count() - 1) self.update_preview() def update_preview(self): - """Update the preview label with the current format string with different styling for elements and static text.""" + """Update the preview label with the current format string including file extension""" preview_html = "" - # preview_html = " for i in range(self.selected_list.count()): - item_text = self.selected_list.item(i).text() + item = self.selected_list.item(i) + item_text = self._get_item_internal_text(item) if item_text.startswith("{Added Text: ") and item_text.endswith("}"): - # Static text - display in blue and bold + # Static text - display the actual text separator = item_text[13:-1] preview_html += f"{separator}" - # preview_html += f"{separator}" else: - # Format element - display in dark green with slight italic + # Format element - display with brackets preview_html += f"{item_text}" - # preview_html += f"{item_text}" - preview_html += "" - # preview_html += "" + # Add the selected file extension + selected_extension = self.file_type_combo.currentText() + if selected_extension: + preview_html += selected_extension + self.preview_display.setText(preview_html) def update_tooltip(self, item): @@ -836,33 +882,31 @@ def update_tooltip(self, item): "Month": "Calendar month in numerical format (1-12)", "VidNum": "Video number identifier", "Year": "Calendar year (4 digits)", - "{Added Text:": "Static text that will appear literally in the filename" } if not item: self.tooltip_label.setText("Hover over an element to see its description") return - item_text = item.text() - - # Handle selected list items (which might have formatting) - if item_text.startswith("{") and item_text.endswith("}"): - if item_text.startswith("{Added Text:"): - # Static text separator + # Check if this is from the selected list (has internal data) + if hasattr(item, 'data') and item.data(Qt.UserRole): + internal_text = item.data(Qt.UserRole) + if internal_text.startswith("{Added Text:"): self.tooltip_label.setText("Static text that will appear literally in the filename") - else: + return + elif internal_text.startswith("{") and internal_text.endswith("}"): # Format element - extract the base name - element = item_text[1:-1] # Remove braces + element = internal_text[1:-1] # Remove braces if ':' in element: element = element.split(':')[0] # Get base element before width spec - - # Get the tooltip or fall back to available elements tooltip - tooltip = tooltips.get(element, tooltips.get(item_text, "No description available")) + tooltip = tooltips.get(element, "No description available") self.tooltip_label.setText(f"{element}: {tooltip}") - else: - # Available list item - tooltip = tooltips.get(item_text, "No description available") - self.tooltip_label.setText(f"{item_text}: {tooltip}") + return + + # Handle available list items or items without internal data + item_text = item.text() + tooltip = tooltips.get(item_text, "No description available") + self.tooltip_label.setText(f"{item_text}: {tooltip}") def show_context_menu(self, position): """Show context menu for the selected list items""" @@ -882,25 +926,27 @@ def show_context_menu(self, position): action = menu.exec(self.selected_list.mapToGlobal(position)) if action == edit_action: - item_text = item.text() + item_text = self._get_item_internal_text(item) # Check if it's a separator if item_text.startswith("{Added Text: ") and item_text.endswith("}"): old_text = item_text[13:-1] # Extract text between "{Added Text: " and "}" new_text, ok = QInputDialog.getText(self, "Edit Added Text", "Edit added text:", text=old_text) if ok: - item.setText(f"{{Added Text: {new_text}}}") + item.setData(Qt.UserRole, f"{{Added Text: {new_text}}}") + item.setText(new_text) # Display just the text elif action == remove_action: self.remove_selected_element() self.update_preview() def get_format_string(self): - """Return the complete format string as plain text without HTML formatting""" + """Return the complete format string including file extension""" format_string = "" for i in range(self.selected_list.count()): - item_text = self.selected_list.item(i).text() + item = self.selected_list.item(i) + item_text = self._get_item_internal_text(item) if item_text.startswith("{Added Text: ") and item_text.endswith("}"): # Static text - just add the text part @@ -910,6 +956,11 @@ def get_format_string(self): # Format element - add as is format_string += item_text + # Add the selected file extension + selected_extension = self.file_type_combo.currentText() + if selected_extension: + format_string += selected_extension + return format_string def get_formatted_preview(self): @@ -931,12 +982,12 @@ class FormatEditorWidget(QWidget): formatChanged = Signal(str) copyToAllRequested = Signal(str, str, str) - def __init__(self, label_text, default_format="", parent=None, queryloc=False, section_name=None, format_key=None): + def __init__(self, label_text, default_format="", parent=None, type=None, section_name=None, format_key=None): super().__init__(parent) self.default_format = default_format self.current_format = default_format # Initialize with default self.return_text = default_format # Initialize return text with default - self.queryloc = queryloc + self.type = type self.section_name = section_name self.format_key = format_key @@ -957,7 +1008,7 @@ def __init__(self, label_text, default_format="", parent=None, queryloc=False, s def _open_format_editor(self): """Open the format editor dialog""" - dialog = FormatElementsEditor(self.current_format, self, self.queryloc, self.section_name, self.format_key) + dialog = FormatElementsEditor(self.current_format, self, self.type, self.section_name, self.format_key) dialog.copyRequested.connect(self.relay_copy_request) @@ -1234,10 +1285,6 @@ def __init__(self, title="", default=None, parent=None): border: none; text-align: left; padding: 5px; - color: black; - } - QToolButton:checked { - background-color: #f0f0f0; } QToolButton:disabled { color: gray; @@ -1671,7 +1718,7 @@ def set_value(self, value): """Set the current modality""" self.comboBox.setCurrentText(value if value else "null") -class freeNumber(QWidget): +class freeFloat(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -1696,6 +1743,31 @@ def get_text(self): elif text: return text +class freeInt(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + main_layout = QVBoxLayout(self) + self.textbox = QLineEdit(self) + int_validator = QIntValidator(-999, 999) + self.textbox.setValidator(int_validator) + main_layout.addWidget(self.textbox) + + def set_text(self, text): + self.textbox.setText(text) + + def get_text(self): + if isinstance(self.textbox, QTextEdit): + text = self.textbox.toPlainText() + else: + text = self.textbox.text() + + # Return placeholder text if the field is empty + if text == "null" or "": + return None + elif text: + return text + class rangeSelector(QWidget): def __init__(self, def_min = None, def_max = None, parent=None): super().__init__(parent) diff --git a/ocvl/function/gui/import_generation.py b/ocvl/function/gui/import_generation.py index d7ce21c..4cb282c 100644 --- a/ocvl/function/gui/import_generation.py +++ b/ocvl/function/gui/import_generation.py @@ -15,31 +15,36 @@ def extract_widget_type(field_def): return field_def.get("type") return field_def +def create_format_editor_widget(config_dict): + """Create FormatEditorWidget with appropriate type based on config""" + format_type = config_dict.get("format_type", "Format") + return FormatEditorWidget(format_type) + WIDGET_FACTORY = { - "freeText": lambda: FreetextBox(), - "freeNumber": lambda: freeNumber(), # or use a QSpinBox/DoubleSpinBox if you make one - "trueFalse": lambda: TrueFalseSelector(), - "comboBox": lambda: DropdownMenu(default="null"), - "outputSubfolderMethodComboBox": lambda: DropdownMenu(options=["DateTime", "Date", "Sequential"]), - "shapeComboBox": lambda: DropdownMenu(default="null", options=["disk", "box"]), - "summaryComboBox": lambda: DropdownMenu(default="null", options=["mean", "median"]), - "typeComboBox": lambda: DropdownMenu(default="null", options=["stim-relative", "absolute"]), - "unitsComboBox": lambda: DropdownMenu(default="null", options=["time", "frames"]), - "standardizationMethodComboBox": lambda: DropdownMenu(default="null", + "freeText": lambda config=None: FreetextBox(), + "freeFloat": lambda config=None: freeFloat(), # or use a QSpinBox/DoubleSpinBox if you make one + "freeInt": lambda config=None: freeInt(), + "trueFalse": lambda config=None: TrueFalseSelector(), + "comboBox": lambda config=None: DropdownMenu(default="null"), + "outputSubfolderMethodComboBox": lambda config=None: DropdownMenu(options=["DateTime", "Date", "Sequential"]), + "shapeComboBox": lambda config=None: DropdownMenu(default="null", options=["disk", "box"]), + "summaryComboBox": lambda config=None: DropdownMenu(default="null", options=["mean", "median"]), + "typeComboBox": lambda config=None: DropdownMenu(default="null", options=["stim-relative", "absolute"]), + "unitsComboBox": lambda config=None: DropdownMenu(default="null", options=["time", "frames"]), + "standardizationMethodComboBox": lambda config=None: DropdownMenu(default="null", options=["mean_stddev", "stddev", "linear_stddev", "linear_vast", "relative_change", "none"]), - "summaryMethodComboBox": lambda: DropdownMenu(default="null", options=["rms", "stddev", "var", "avg"]), - "controlComboBox": lambda: DropdownMenu(default="null", options=["none", "subtraction", "division"]), - "listEditor": lambda: ListEditorWidget(), - "openFolder": lambda: OpenFolder(), - "formatEditor": lambda: FormatEditorWidget("Format"), - "groupbyEditor": lambda: GroupByFormatEditorWidget(None, None, None, "Group By"), - "formatEditorQueryloc": lambda: FormatEditorWidget("Format", queryloc=True), - "cmapSelector": lambda: ColorMapSelector(), - "affineRigidSelector": lambda: AffineRigidSelector(), - "saveasSelector": lambda: SaveasExtensionsEditorWidget("Save as"), - "rangeSelector": lambda: rangeSelector(), - "null": lambda: QLabel("null"), + "summaryMethodComboBox": lambda config=None: DropdownMenu(default="null", options=["rms", "stddev", "var", "avg"]), + "controlComboBox": lambda config=None: DropdownMenu(default="null", options=["none", "subtraction", "division"]), + "listEditor": lambda config=None: ListEditorWidget(), + "openFolder": lambda config=None: OpenFolder(), + "formatEditor": lambda config=None: create_format_editor_widget(config or {}), + "groupbyEditor": lambda config=None: GroupByFormatEditorWidget(None, None, None, "Group By"), + "cmapSelector": lambda config=None: ColorMapSelector(), + "affineRigidSelector": lambda config=None: AffineRigidSelector(), + "saveasSelector": lambda config=None: SaveasExtensionsEditorWidget("Save as"), + "rangeSelector": lambda config=None: rangeSelector(), + "null": lambda config=None: QLabel("null"), } def build_form_from_template(template: dict, data: dict, adv=False, parent_name="", saved_widgets=None) -> QWidget: @@ -328,14 +333,10 @@ def generate_json(form_container, template): # Convert string values to appropriate types if needed if value is not None: - if widget_type in ["freeNumber"]: - try: - if '.' in str(value): - value = float(value) - else: - value = int(value) - except ValueError: - pass # Keep as string if conversion fails + if widget_type in ["freeInt"]: + value = int(value) + elif widget_type in ["freeFloat"]: + value = float(value) elif widget_type == "trueFalse": value = bool(value) elif widget_type == "null": diff --git a/ocvl/function/gui/wizard_creator.py b/ocvl/function/gui/wizard_creator.py index d558fd7..2a9f8e3 100644 --- a/ocvl/function/gui/wizard_creator.py +++ b/ocvl/function/gui/wizard_creator.py @@ -102,8 +102,8 @@ def update_button_text_for_intro(self): class IntroPage(QWizardPage): def __init__(self, parent=None): super().__init__(parent) - self.setTitle("Welcome to the MEAO Configuration JSON Generator!") - self.setSubTitle('To begin, choose if you wish to import an existing config JSON, or create a new one\n' + self.setTitle("Welcome to the MEAO Configuration File Generator!") + self.setSubTitle('To begin, choose if you wish to import an existing config file, or create a new one\n' '• Note: Importing an existing config file will bring you to "advanced" setup') self.imported_config = None # To store the imported config @@ -126,12 +126,39 @@ def __init__(self, parent=None): # Label label = QLabel("Select an option:") - label.setToolTip("Test1") center_layout.addWidget(label) - # Radio buttons - self.create_button = QRadioButton("Create New Configuration JSON") - self.import_button = QRadioButton("Import Existing Configuration JSON") + self.create_button = QRadioButton("Create New Configuration") + self.import_button = QRadioButton("Import Existing Configuration") + + # Style to make buttons bigger + radio_style = """ + QRadioButton { + min-width: 350px; + min-height: 45px; + font-size: 20px; + padding: 5px; + spacing: 5px; + } + QRadioButton::indicator { + width: 20px; + height: 20px; + } + """ + label_style = """ + QLabel { + min-width: 350px; + min-height: 30px; + font-size: 20px; + font-weight: bold; + padding: 5px; + qproperty-alignment: AlignCenter; + } + """ + + label.setStyleSheet(label_style) + self.create_button.setStyleSheet(radio_style) + self.import_button.setStyleSheet(radio_style) self.button_group = QButtonGroup() self.button_group.addButton(self.create_button, 0) @@ -183,8 +210,8 @@ class SelectionPage(QWizardPage): def __init__(self, parent=None): super().__init__(parent) self.setTitle('Choose your desired type of setup and click "Next"') - self.setSubTitle("• Simple generation: Step-by-step process to create configuration JSON\n" - "• Advanced generation: In-depth menu with access to change any and all fields in the configuration JSON in one step") + self.setSubTitle("• Simple generation: Step-by-step process to create configuration file\n" + "• Advanced generation: In-depth menu with access to change any and all fields in the configuration file in one step") # Create scrollable area scroll = QScrollArea() @@ -198,17 +225,73 @@ def __init__(self, parent=None): outer_layout = QVBoxLayout(container) outer_layout.setAlignment(Qt.AlignCenter) # Center everything - # Inner layout: label and buttons side by side center_layout = QHBoxLayout() center_layout.setAlignment(Qt.AlignCenter) + placeholder = "Hover over an option to see details" + + # Tooltip label + self.tooltip_label = QLabel(placeholder) + self.tooltip_label.setWordWrap(True) + self.tooltip_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.tooltip_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + self.tooltip_label.setStyleSheet(""" + QLabel { + border: 1px solid #ccc; + border-radius: 6px; + background-color: #f0f0f0; + padding: 6px 12px; + font-style: italic; + color: #333; + min-height: 40px; + } + """) + + def create_hover_widget(layout, tooltip_text): + wrapper = QFrame() + wrapper.setLayout(layout) + wrapper.setMouseTracking(True) + wrapper.setStyleSheet("QFrame { background: transparent; }") + wrapper.enterEvent = lambda event: self.tooltip_label.setText(tooltip_text) + wrapper.leaveEvent = lambda event: self.tooltip_label.setText(placeholder) + return wrapper + + # Label label = QLabel("Choose type of generation:") center_layout.addWidget(label) # Radio buttons - self.simple_button = QRadioButton("Simple") - self.adv_button = QRadioButton("Advanced") + self.simple_button = QRadioButton("Simple Generation") + self.adv_button = QRadioButton("Advanced Generation") + + radio_style = """ + QRadioButton { + min-width: 350px; + min-height: 45px; + font-size: 20px; + padding: 5px; + spacing: 5px; + } + QRadioButton::indicator { + width: 20px; + height: 20px; + } + """ + label_style = """ + QLabel { + min-width: 350px; + min-height: 30px; + font-size: 20px; + font-weight: bold; + padding: 5px; + qproperty-alignment: AlignCenter; + } + """ + + self.simple_button.setStyleSheet(radio_style) + self.adv_button.setStyleSheet(radio_style) + label.setStyleSheet(label_style) self.button_group = QButtonGroup() self.button_group.addButton(self.simple_button, 0) @@ -223,6 +306,7 @@ def __init__(self, parent=None): center_layout.addLayout(button_layout) + # Add to outer layout outer_layout.addLayout(center_layout) @@ -370,7 +454,7 @@ def create_hover_widget(layout, tooltip_text): image_layout = QHBoxLayout() image_layout.setAlignment(Qt.AlignLeft) image_label = QLabel("Image Format:") - self.image_format_value = constructors.FormatEditorWidget("Image Format:", "{IDnum}{Year}{Month}{Day}{VidNum}{Modality}") + self.image_format_value = constructors.FormatEditorWidget("Image Format:", "{IDnum}_{Year}{Month}{Day}_{Eye}_({LocX},{LocY})_{FOV_Width}x{FOV_Height}_{VidNum}_{Modality}.tif", type='image') image_layout.addWidget(image_label) image_layout.addWidget(self.image_format_value) image_widget = create_hover_widget(image_layout, "Image Format: Format string for image filenames.") @@ -379,7 +463,7 @@ def create_hover_widget(layout, tooltip_text): video_layout = QHBoxLayout() video_layout.setAlignment(Qt.AlignLeft) video_label = QLabel("Video Format:") - self.video_format_value = constructors.FormatEditorWidget("Video Format:", "{IDnum}{Year}{Month}{Day}{VidNum}{Modality}") + self.video_format_value = constructors.FormatEditorWidget("Video Format:", "{IDnum}_{Year}{Month}{Day}_{Eye}_({LocX},{LocY})_{FOV_Width}x{FOV_Height}_{VidNum}_{Modality}.avi", type='video') video_layout.addWidget(video_label) video_layout.addWidget(self.video_format_value) video_widget = create_hover_widget(video_layout, "Video Format: Format string for video filenames.") @@ -388,7 +472,7 @@ def create_hover_widget(layout, tooltip_text): mask_layout = QHBoxLayout() mask_layout.setAlignment(Qt.AlignLeft) mask_label = QLabel("Mask Format:") - self.mask_format_value = constructors.FormatEditorWidget("Mask Format:", "{IDnum}{Year}{Month}{Day}{VidNum}{Modality}") + self.mask_format_value = constructors.FormatEditorWidget("Mask Format:", "{IDnum}_{Year}{Month}{Day}_{Eye}_({LocX},{LocY})_{FOV_Width}x{FOV_Height}_{VidNum}_{Modality}.avi", type='mask') mask_layout.addWidget(mask_label) mask_layout.addWidget(self.mask_format_value) mask_widget = create_hover_widget(mask_layout, "Mask Format: Format string for mask filenames.") @@ -560,7 +644,7 @@ def create_hover_widget(layout, tooltip_text): image_layout = QHBoxLayout() image_layout.setAlignment(Qt.AlignLeft) image_label = QLabel("Image Format:") - self.image_format_value = constructors.FormatEditorWidget("Image Format:", "{IDnum}{Year}{Month}{Day}{VidNum}{Modality}") + self.image_format_value = constructors.FormatEditorWidget("Image Format:", "{IDnum}_{Year}{Month}{Day}_{Eye}_({LocX},{LocY})_{FOV_Width}x{FOV_Height}_{VidNum}_{Modality}.tif", type='image') image_layout.addWidget(image_label) image_layout.addWidget(self.image_format_value) image_widget = create_hover_widget(image_layout, "Image Format: Format string for image filenames.") @@ -568,7 +652,7 @@ def create_hover_widget(layout, tooltip_text): queryloc_layout = QHBoxLayout() queryloc_layout.setAlignment(Qt.AlignLeft) queryloc_label = QLabel("Query Loc Format:") - self.queryloc_format_value = constructors.FormatEditorWidget("Queryloc Format:", "{IDnum}{Year}{Month}{Day}{VidNum}{Modality}", queryloc=True) + self.queryloc_format_value = constructors.FormatEditorWidget("Queryloc Format:", "{IDnum}_{Year}{Month}{Day}_{Eye}_({LocX},{LocY})_{FOV_Width}x{FOV_Height}_{VidNum}_{Modality}.csv", type='queryloc') queryloc_layout.addWidget(queryloc_label) queryloc_layout.addWidget(self.queryloc_format_value) queryloc_widget = create_hover_widget(queryloc_layout, "Query Loc Format: Filename format used to locate the reference location (query).") @@ -576,7 +660,7 @@ def create_hover_widget(layout, tooltip_text): video_layout = QHBoxLayout() video_layout.setAlignment(Qt.AlignLeft) video_label = QLabel("Video Format:") - self.video_format_value = constructors.FormatEditorWidget("Video Format:", "{IDnum}{Year}{Month}{Day}{VidNum}{Modality}") + self.video_format_value = constructors.FormatEditorWidget("Video Format:", "{IDnum}_{Year}{Month}{Day}_{Eye}_({LocX},{LocY})_{FOV_Width}x{FOV_Height}_{VidNum}_{Modality}.avi", type='video') video_layout.addWidget(video_label) video_layout.addWidget(self.video_format_value) video_widget = create_hover_widget(video_layout, "Video Format: Format string for video filenames.") @@ -657,9 +741,9 @@ def __init__(self, parent=None): 'You can edit any configuration field from here. For a more simple setup, go back and select "Simple Setup"\n' 'Tip: Expand window for better visibility') - with open("config_files/advanced_config_JSON.json", "r") as f: + with open("master_config_files/advanced_config_JSON.json", "r") as f: advanced_config_json = json.load(f) - with open("config_files/master_JSON.json", "r") as f: + with open("master_config_files/master_JSON.json", "r") as f: self.master_json = json.load(f) scroll_area = QScrollArea() From 04c42f30d56917aa4201cc6d8849567d2511f44e Mon Sep 17 00:00:00 2001 From: Niko Rios Date: Thu, 30 Oct 2025 10:18:40 -0500 Subject: [PATCH 2/4] Import page now routes to new review page --- .idea/misc.xml | 2 +- .idea/pyORG_Calculation.iml | 2 +- ocvl/function/gui/constructors.py | 69 ++- ocvl/function/gui/import_generation.py | 124 ++++-- .../gui/master_config_files/master_JSON.json | 22 + ocvl/function/gui/wizard_creator.py | 421 +++++++++++------- 6 files changed, 444 insertions(+), 196 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index a909caa..f143d42 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/.idea/pyORG_Calculation.iml b/.idea/pyORG_Calculation.iml index 3437622..201e3f1 100644 --- a/.idea/pyORG_Calculation.iml +++ b/.idea/pyORG_Calculation.iml @@ -6,7 +6,7 @@ - + \ No newline at end of file diff --git a/ocvl/function/gui/constructors.py b/ocvl/function/gui/constructors.py index 545756a..4ee0dc2 100644 --- a/ocvl/function/gui/constructors.py +++ b/ocvl/function/gui/constructors.py @@ -343,6 +343,7 @@ def __init__(self, current_format=None, parent=None, type=None, section_name=Non self.type = type self.extension_options = self._get_extension_options() + self.existing_extension = None # Track existing extension from imported format self.copy_button = QPushButton("Copy to All in Section") self.copy_button.clicked.connect(self.copy_to_all) @@ -549,6 +550,38 @@ def _get_extension_options(self): else: return [".txt", ".dat", ".log"] + def _detect_existing_extension(self, format_string): + """Detect if the format string already contains a file extension""" + if not format_string: + return None + + # Common file extensions to check for + common_extensions = ['.tif', '.png', '.jpg', '.jpeg', '.avi', '.mov', '.mat', '.npy', + '.txt', '.json', '.xml', '.csv', '.log', '.dat'] + + for ext in common_extensions: + if format_string.lower().endswith(ext.lower()): + return ext + + # Check for any extension pattern at the end (dot followed by 2-5 characters) + import re + match = re.search(r'\.([a-zA-Z0-9]{2,5})$', format_string) + if match: + return '.' + match.group(1) + + return None + + def _add_existing_extension_to_dropdown(self, extension): + """Add the existing extension to the dropdown if it's not already there""" + if extension and extension not in [self.file_type_combo.itemText(i) for i in + range(self.file_type_combo.count())]: + # Add the existing extension at the top of the list + self.file_type_combo.insertItem(0, extension) + # Add a separator item for clarity + self.file_type_combo.insertSeparator(1) + # Set the existing extension as selected + self.file_type_combo.setCurrentText(extension) + def copy_to_all(self): reply = QMessageBox.question( self, @@ -655,13 +688,24 @@ def set_element_width(self): def parse_format(self, format_string): """Parse an existing format string into elements""" + # Check if format string contains an existing extension + self.existing_extension = self._detect_existing_extension(format_string) + + # If we found an existing extension, remove it from the format string for parsing + # and add it to the dropdown + if self.existing_extension: + format_without_extension = format_string[:-len(self.existing_extension)] + self._add_existing_extension_to_dropdown(self.existing_extension) + else: + format_without_extension = format_string + # First reset available elements self.available_elements = self.original_elements.copy() self.available_list.clear() self.available_list.addItems(self.available_elements) # Parse the string by looking for elements enclosed in brackets - remaining = format_string + remaining = format_without_extension # Process the format string while remaining: @@ -860,13 +904,28 @@ def update_preview(self): # Format element - display with brackets preview_html += f"{item_text}" - # Add the selected file extension + # Add the selected file extension only if we don't already have an extension in the format selected_extension = self.file_type_combo.currentText() - if selected_extension: + if selected_extension and not self._has_extension_in_format(): preview_html += selected_extension self.preview_display.setText(preview_html) + def _has_extension_in_format(self): + """Check if the current format already contains a file extension""" + format_without_dropdown = "" + for i in range(self.selected_list.count()): + item = self.selected_list.item(i) + item_text = self._get_item_internal_text(item) + + if item_text.startswith("{Added Text: ") and item_text.endswith("}"): + separator = item_text[13:-1] + format_without_dropdown += separator + else: + format_without_dropdown += item_text + + return self._detect_existing_extension(format_without_dropdown) is not None + def update_tooltip(self, item): """Update tooltip box when hovering over available or selected elements.""" tooltips = { @@ -956,9 +1015,9 @@ def get_format_string(self): # Format element - add as is format_string += item_text - # Add the selected file extension + # Add the selected file extension only if we don't already have one in the format selected_extension = self.file_type_combo.currentText() - if selected_extension: + if selected_extension and not self._has_extension_in_format(): format_string += selected_extension return format_string diff --git a/ocvl/function/gui/import_generation.py b/ocvl/function/gui/import_generation.py index 4cb282c..fe41516 100644 --- a/ocvl/function/gui/import_generation.py +++ b/ocvl/function/gui/import_generation.py @@ -21,6 +21,7 @@ def create_format_editor_widget(config_dict): return FormatEditorWidget(format_type) WIDGET_FACTORY = { + # Main fields "freeText": lambda config=None: FreetextBox(), "freeFloat": lambda config=None: freeFloat(), # or use a QSpinBox/DoubleSpinBox if you make one "freeInt": lambda config=None: freeInt(), @@ -45,8 +46,24 @@ def create_format_editor_widget(config_dict): "saveasSelector": lambda config=None: SaveasExtensionsEditorWidget("Save as"), "rangeSelector": lambda config=None: rangeSelector(), "null": lambda config=None: QLabel("null"), + + # Subfields + "text_file": lambda config=None: QLabel("text_file"), # For metadata type + "folder": lambda config=None: QLabel("folder"), # For control location + "score": lambda config=None: QLabel("score"), # For normalization method + "mean_sub": lambda config=None: QLabel("mean_sub"), # For standardization method + "auto": lambda config=None: QLabel("auto"), # For radius + "disk": lambda config=None: QLabel("disk"), # For shape + "mean": lambda config=None: QLabel("mean"), # For summary + "rms": lambda config=None: QLabel("rms"), # For summary method + "subtraction": lambda config=None: QLabel("subtraction"), # For control + "stim-relative": lambda config=None: QLabel("stim-relative"), # For type + "time": lambda config=None: QLabel("time"), # For units + "viridis": lambda config=None: QLabel("viridis"), # For cmap + "plasma": lambda config=None: QLabel("plasma"), # For cmap } + def build_form_from_template(template: dict, data: dict, adv=False, parent_name="", saved_widgets=None) -> QWidget: if saved_widgets is None: saved_widgets = {} @@ -87,30 +104,49 @@ def build_form_from_template(template: dict, data: dict, adv=False, parent_name= dependencies = None if not widget_type or widget_type not in WIDGET_FACTORY: - continue + # If no widget type is defined, create a default widget based on value type + if isinstance(val, bool): + widget_type = "trueFalse" + elif isinstance(val, (int, float)): + widget_type = "freeText" # Use freeText for numbers + elif isinstance(val, list): + widget_type = "listEditor" + elif val is None: + widget_type = "null" + else: + widget_type = "freeText" widget_constructor = WIDGET_FACTORY.get(widget_type) + if not widget_constructor: + continue + field_widget = widget_constructor() if isinstance(field_widget, FormatEditorWidget): - field_widget.section_name = parent_name # e.g., "preanalysis" or "analysis" + field_widget.section_name = parent_name field_widget.format_key = key field_widget.copyToAllRequested.connect( lambda s, k, v, sw=saved_widgets: propagate_advanced_copy(sw, s, k, v) ) + # Set the value based on the actual data type if val is not None: - if hasattr(field_widget, "set_text"): - field_widget.set_text(str(val)) - elif hasattr(field_widget, "set_value"): - if isinstance(field_widget, ListEditorWidget) and isinstance(val, list): + if hasattr(field_widget, "set_value"): + # Handle different value types appropriately + if isinstance(val, bool): field_widget.set_value(val) - elif isinstance(val, (list, dict)) and not isinstance(field_widget, ListEditorWidget): + elif isinstance(val, (int, float)): + # For numeric values, convert to string for widgets that expect text field_widget.set_value(str(val)) - elif isinstance(val, bool): + elif isinstance(val, list) and isinstance(field_widget, ListEditorWidget): field_widget.set_value(val) + elif isinstance(val, (list, dict)): + # For complex types, convert to string representation + field_widget.set_value(str(val)) else: field_widget.set_value(str(val)) + elif hasattr(field_widget, "set_text"): + field_widget.set_text(str(val)) # Save widget if marked for saving if save_widget and parent_name: @@ -254,7 +290,7 @@ def update_modalities_enabled(): # Initial update update_modalities_enabled() -def generate_json(form_container, template): +def generate_json(form_container, template, skip_disabled=True): result = {} form_layout = form_container.layout() if not form_layout: @@ -268,25 +304,24 @@ def generate_json(form_container, template): # Handle collapsible sections (nested objects) if isinstance(widget, CollapsibleSection): - if isinstance(widget, CollapsibleSection): - if not widget.is_enabled(): # Skip disabled sections - continue + if skip_disabled and not widget.is_enabled(): + continue - section_title = widget.title().replace(':', '').replace(' ', '_').lower() + section_title = widget.title().replace(':', '').replace(' ', '_').lower() - content_layout = widget.content_area.layout() - if not content_layout: - continue + content_layout = widget.content_area.layout() + if not content_layout: + continue - content_widget = QWidget() - content_widget.setLayout(content_layout) + content_widget = QWidget() + content_widget.setLayout(content_layout) - template_for_section = template.get(section_title, {}) + template_for_section = template.get(section_title, {}) - section_data = generate_json(content_widget, template_for_section) - if section_data: - result[section_title] = section_data - continue + section_data = generate_json(content_widget, template_for_section, skip_disabled) + if section_data: + result[section_title] = section_data + continue # Handle regular form rows if isinstance(widget, QWidget): @@ -307,8 +342,8 @@ def generate_json(form_container, template): # Handle OptionalField wrapper if present if isinstance(field_widget, OptionalField): - if not field_widget.is_checked(): - continue # Skip if the field is disabled + if skip_disabled and not field_widget.is_checked(): + continue field_widget = field_widget.field_widget # Get the widget type from template to determine how to get the value @@ -327,30 +362,43 @@ def generate_json(form_container, template): value = field_widget.get_text() elif hasattr(field_widget, 'get_list'): value = field_widget.get_list() + elif hasattr(field_widget, 'currentText'): # For QComboBox based widgets + value = field_widget.currentText() + elif hasattr(field_widget, 'text'): # For QLabel and similar + value = field_widget.text() + elif hasattr(field_widget, 'isChecked'): # For checkboxes + value = field_widget.isChecked() elif isinstance(field_widget, QLabel): value = field_widget.text() - # Convert string values to appropriate types if needed if value is not None: - if widget_type in ["freeInt"]: + # Handle numeric values first + if isinstance(value, str): + # Try to convert to int or float if possible + try: + if '.' in value: + value = float(value) + else: + value = int(value) + except (ValueError, TypeError): + # If conversion fails, handle special string cases + if value.lower() == "null": + value = None + elif value.lower() == "true": + value = True + elif value.lower() == "false": + value = False + elif widget_type in ["freeInt"] and isinstance(value, (int, float)): value = int(value) - elif widget_type in ["freeFloat"]: + elif widget_type in ["freeFloat"] and isinstance(value, (int, float)): value = float(value) elif widget_type == "trueFalse": value = bool(value) elif widget_type == "null": value = None - elif isinstance(value, str): - # Handle special string cases - if value.lower() == "null": - value = None - elif value.lower() == "true": - value = True - elif value.lower() == "false": - value = False - - # Only add to result if we got a value (including None) + + # Always add to result, even if value is None result[key] = value return result \ No newline at end of file diff --git a/ocvl/function/gui/master_config_files/master_JSON.json b/ocvl/function/gui/master_config_files/master_JSON.json index 35d3c96..00bdc0d 100644 --- a/ocvl/function/gui/master_config_files/master_JSON.json +++ b/ocvl/function/gui/master_config_files/master_JSON.json @@ -5,6 +5,28 @@ "description": { "type": "freeText" }, + "raw" : { + "video_format": { + "type": "formatEditor", + "save": true + }, + "metadata" : { + "type": { + "type": "freeText" + }, + "metadata_format": { + "type": "formatEditor" + }, + "fields_to_load": { + "timestamps": { + "type": "freeText" + }, + "stimulus_train": { + "type": "freeText" + } + } + } + }, "preanalysis": { "image_format": { "type": "formatEditor", diff --git a/ocvl/function/gui/wizard_creator.py b/ocvl/function/gui/wizard_creator.py index 2a9f8e3..71e28db 100644 --- a/ocvl/function/gui/wizard_creator.py +++ b/ocvl/function/gui/wizard_creator.py @@ -3,15 +3,11 @@ from PySide6 import QtGui from PySide6.QtGui import QFont, QMovie -from PySide6.QtWidgets import (QApplication, QWizard, QWizardPage, - QLabel, QLineEdit, QVBoxLayout, - QCheckBox, QComboBox, QHBoxLayout, QRadioButton, QButtonGroup, QSizePolicy, QScrollArea, - QWidget, QFileDialog, QMessageBox, QFrame) -from PySide6.QtCore import Qt, QSize, Signal -from advancedconfig import create_advanced_setup_widget, description_layer +from PySide6.QtWidgets import QWizard import constructors -import advancedconfig from import_generation import * +from ocvl.function.utility.dataset import parse_metadata +import tempfile bold = QtGui.QFont() bold.setBold(True) @@ -84,8 +80,10 @@ def update_button_text(self, id): if id == 0: # On intro page, check which option is selected self.update_button_text_for_intro() - elif id == 6 | 7: + elif id == 6: # Review page self.button(QWizard.NextButton).setText("Save >") + elif id == 7: # Import editor page + self.button(QWizard.NextButton).setText("Review >") # Or "Next >" else: self.button(QWizard.NextButton).setText("Next >") @@ -741,9 +739,9 @@ def __init__(self, parent=None): 'You can edit any configuration field from here. For a more simple setup, go back and select "Simple Setup"\n' 'Tip: Expand window for better visibility') - with open("master_config_files/advanced_config_JSON.json", "r") as f: + with open("ocvl/function/gui/master_config_files/advanced_config_JSON.json", "r") as f: advanced_config_json = json.load(f) - with open("master_config_files/master_JSON.json", "r") as f: + with open("ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: self.master_json = json.load(f) scroll_area = QScrollArea() @@ -771,29 +769,20 @@ def __init__(self, parent=None): self.saved_file_path = None self.generated_config = None - self.layout = QVBoxLayout(self) - self.label = QLabel() - self.label.setWordWrap(True) - - self.label2 = QLabel() - self.label2.setWordWrap(True) - - self.label3 = QLabel() - self.label3.setWordWrap(True) + # Create scrollable area for the review content + self.scroll = QScrollArea() + self.scroll.setWidgetResizable(True) - self.label4 = QLabel() - self.label4.setWordWrap(True) + # Container widget for all content + self.container = QWidget() + self.scroll.setWidget(self.container) - self.label5 = QLabel() - self.label5.setWordWrap(True) - - self.layout.addWidget(self.label) - self.layout.addWidget(self.label2) - self.layout.addWidget(self.label3) - self.layout.addWidget(self.label4) - self.layout.addWidget(self.label5) + # Main layout + self.main_layout = QVBoxLayout(self) + self.main_layout.addWidget(self.scroll) - self.layout.addStretch() + # Container layout + self.container_layout = QVBoxLayout(self.container) def nextId(self): return 8 @@ -801,85 +790,260 @@ def nextId(self): def initializePage(self): wizard = self.wizard() - if wizard.page(1).adv_button.isChecked(): - self.label.setText( - "Advanced setup selected.\n\nPlease review your inputs on the previous page before continuing.") - self.generated_config = None # Don't regenerate config in advanced mode - return - - # Simple mode: generate config with only simple-mode configurable elements - config = { - "version": wizard.page(2).version_value.text(), - "description": wizard.page(2).description_value.text(), - "preanalysis": { - "image_format": wizard.page(3).image_format_value.get_value(), - "video_format": wizard.page(3).video_format_value.get_value(), - "mask_format": wizard.page(3).mask_format_value.get_value(), - "recursive_search" : wizard.page(3).recursive_search_tf.get_value(), - "pipeline_params": { - "modalities": wizard.page(3).modalities_list_creator.get_list(), - "alignment_reference_modality" : wizard.page(3).alignment_ref_value.get_value(), - "group_by": None if wizard.page(3).groupby_value.get_value() == "null" else wizard.page(3).groupby_value.get_value(), - } - }, - "analysis": { - "image_format": wizard.page(4).image_format_value.get_value(), - "queryloc_format": wizard.page(4).queryloc_format_value.get_value(), - "video_format": wizard.page(4).video_format_value.get_value(), - "recursive_search": wizard.page(4).recursive_search_tf.get_value(), - "analysis_params": { - "modalities": wizard.page(4).modalities_list_creator.get_list() - } - } - } - - self.generated_config = config - - # Show summary to user - summary_text1 = f"Version: {config['version']}\n" \ - f"Description: {config['description']}\n" - - summary_text2 = f"Image Format: {config['preanalysis']['image_format']}\n" \ - f"Video Format: {config['preanalysis']['video_format']}\n" \ - f"Mask Format: {config['preanalysis']['mask_format']}\n" \ - f"Recursive Search: {config['preanalysis']['recursive_search']}\n" \ - f"Modalities: {config['preanalysis']['pipeline_params']['modalities']}\n" \ - f"Alignment Reference Modality : {config['preanalysis']['pipeline_params']['alignment_reference_modality']}\n" \ - f"Group By: {config['preanalysis']['pipeline_params']['group_by']}\n" - - summary_text3 = f"Image Format: {config['analysis']['image_format']}\n" \ - f"QueryLoc Format: {config['analysis']['queryloc_format']}\n" \ - f"Video Format: {config['analysis']['video_format']}\n" \ - f"Recursive Search: {config['analysis']['recursive_search']}\n" \ - f"Modalities: {config['analysis']['analysis_params']['modalities']}" - - self.label.setText( - "Please review the following configurations before clicking 'Save >':\n\n" + summary_text1) - self.label2.setText('Pre-Analysis') - self.label2.setFont(bold) - self.label3.setText(summary_text2) - self.label4.setText('Analysis') - self.label4.setFont(bold) - self.label5.setText(summary_text3) - - def validatePage(self): - wizard = self.wizard() + # Clear any existing labels (except header) + for i in reversed(range(1, self.container_layout.count())): + item = self.container_layout.itemAt(i) + if item and item.widget(): + item.widget().deleteLater() + # Generate config based on mode if wizard.page(1).adv_button.isChecked(): - # Advanced mode: generate config from widgets directly + # Advanced mode: use generate_json advanced_widget = wizard.page(5).advanced_widget master_json = wizard.page(5).master_json - config = generate_json(advanced_widget, master_json) + self.generated_config = generate_json(advanced_widget, master_json) + elif wizard.page(0).import_button.isChecked(): + # Import mode: use generate_json with skip_disabled=False + import_config = wizard.page(7).form_widget + master_json = wizard.page(5).master_json + self.generated_config = generate_json(import_config, master_json, skip_disabled=False) else: - # Simple mode: use the generated config from initializePage - config = self.generated_config - if not config: - self.label.setText("No configuration data available") - return False + # Simple mode: use the predefined config structure + config = { + "version": wizard.page(2).version_value.text(), + "description": wizard.page(2).description_value.text(), + "preanalysis": { + "image_format": wizard.page(3).image_format_value.get_value(), + "video_format": wizard.page(3).video_format_value.get_value(), + "mask_format": wizard.page(3).mask_format_value.get_value(), + "recursive_search": wizard.page(3).recursive_search_tf.get_value(), + "pipeline_params": { + "modalities": wizard.page(3).modalities_list_creator.get_list(), + "alignment_reference_modality": wizard.page(3).alignment_ref_value.get_value(), + "group_by": None if wizard.page(3).groupby_value.get_value() == "null" else wizard.page( + 3).groupby_value.get_value(), + } + }, + "analysis": { + "image_format": wizard.page(4).image_format_value.get_value(), + "queryloc_format": wizard.page(4).queryloc_format_value.get_value(), + "video_format": wizard.page(4).video_format_value.get_value(), + "recursive_search": wizard.page(4).recursive_search_tf.get_value(), + "analysis_params": { + "modalities": wizard.page(4).modalities_list_creator.get_list() + } + } + } + self.generated_config = config + + # Parse and display the configuration + self.display_parsed_config() + + def create_field_label(self, field_name, field_value): + """Create a QLabel for a field with name and value""" + label = QLabel(f"{field_name}: {field_value}") + label.setWordWrap(True) + label.setStyleSheet(""" + QLabel { + padding: 5px; + margin: 2px 0; + border-left: 3px solid #007ACC; + } + """) + return label + + def create_section_label(self, section_name): + """Create a bolded section header label""" + label = QLabel(section_name.upper()) + label.setFont(bold) + label.setStyleSheet(""" + QLabel { + font-size: 14px; + font-weight: bold; + margin: 10px 0 5px 0; + padding: 5px; + } + """) + return label + + def display_parsed_config(self): + """Parse the configuration and display results as individual QLabels""" + if not self.generated_config: + error_label = QLabel("No configuration data available") + self.container_layout.addWidget(error_label) + return + + try: + wizard = self.wizard() + is_advanced_mode = wizard.page(1).adv_button.isChecked() + + is_import_mode = wizard.page(0).import_button.isChecked() + + # 1. Display version and description first + if "version" in self.generated_config: + version_label = self.create_field_label("Version", self.generated_config["version"]) + self.container_layout.addWidget(version_label) + + if "description" in self.generated_config: + description_label = self.create_field_label("Description", self.generated_config["description"]) + self.container_layout.addWidget(description_label) + + if is_advanced_mode or is_import_mode: + # Advanced mode: Display all fields dynamically + self.display_all_fields(self.generated_config) + else: + # Simple mode: Display known fields only + self.display_simple_mode_fields() + + # Add stretch at the end + self.container_layout.addStretch() + + except Exception as e: + error_label = QLabel(f"Error parsing configuration: {str(e)}") + error_label.setStyleSheet("color: red; font-weight: bold;") + self.container_layout.addWidget(error_label) + + def display_all_fields(self, config): + """Display all fields in the configuration recursively for advanced mode""" + # Process sections in the desired order + ordered_sections = ["raw", "preanalysis", "analysis"] + + # First display known sections in order + for section_key in ordered_sections: + if section_key in config: + section_label = self.create_section_label(section_key.replace("_", " ")) + self.container_layout.addWidget(section_label) + + # Try to parse with parse_metadata if possible + try: + parsed_config, parsed_df = parse_metadata(config, root_group=section_key) + except: + pass # Continue if parsing fails + + # Display all fields in this section + self.display_dict_fields(config[section_key]) + + # Then display any additional sections + for key, value in config.items(): + if key not in ["version", "description"] + ordered_sections: + section_label = self.create_section_label(key.replace("_", " ")) + self.container_layout.addWidget(section_label) + + if isinstance(value, dict): + self.display_dict_fields(value) + else: + field_label = self.create_field_label(key.replace("_", " ").title(), value) + self.container_layout.addWidget(field_label) + + def display_dict_fields(self, dictionary, prefix=""): + """Recursively display all fields in a dictionary""" + for key, value in dictionary.items(): + display_key = f"{prefix}{key.replace('_', ' ').title()}" if prefix else key.replace('_', ' ').title() + + if isinstance(value, dict): + # If it's a nested dict, create a sub-section or display with indentation + if len(value) > 3: # Create subsection for large nested dicts + subsection_label = QLabel(f" {display_key}:") + subsection_label.setStyleSheet(""" + QLabel { + font-weight: bold; + margin: 5px 0 2px 10px; + padding: 3px; + } + """) + self.container_layout.addWidget(subsection_label) + self.display_dict_fields(value, " ") + else: + # For small nested dicts, display inline + for sub_key, sub_value in value.items(): + nested_display_key = f"{display_key} - {sub_key.replace('_', ' ').title()}" + field_label = self.create_field_label(nested_display_key, sub_value) + self.container_layout.addWidget(field_label) + else: + # Use the improved create_field_label method for all values + # (it now handles lists, numbers, booleans, etc. internally) + field_label = self.create_field_label(display_key, value) + self.container_layout.addWidget(field_label) + + def display_simple_mode_fields(self): + """Display only the fields configured in simple mode""" + # 2. Display preanalysis section + if "preanalysis" in self.generated_config: + section_label = self.create_section_label("Pre-Analysis") + self.container_layout.addWidget(section_label) + + # Parse with updated function signature + preanalysis_config, preanalysis_df = parse_metadata( + self.generated_config, root_group="preanalysis" + ) + + preanalysis_section = self.generated_config["preanalysis"] + + # Display main preanalysis fields + for field_name, field_key in [ + ("Image Format", "image_format"), + ("Video Format", "video_format"), + ("Mask Format", "mask_format"), + ("Recursive Search", "recursive_search") + ]: + if field_key in preanalysis_section: + field_label = self.create_field_label(field_name, preanalysis_section[field_key]) + self.container_layout.addWidget(field_label) + + # Display pipeline_params + if "pipeline_params" in preanalysis_section: + params = preanalysis_section["pipeline_params"] + for field_name, field_key in [ + ("Modalities", "modalities"), + ("Alignment Reference Modality", "alignment_reference_modality"), + ("Group By", "group_by") + ]: + if field_key in params: + field_label = self.create_field_label(field_name, params[field_key]) + self.container_layout.addWidget(field_label) + + # 3. Display analysis section + if "analysis" in self.generated_config: + section_label = self.create_section_label("Analysis") + self.container_layout.addWidget(section_label) + + # Parse with updated function signature + analysis_config, analysis_df = parse_metadata( + self.generated_config, root_group="analysis" + ) + + analysis_section = self.generated_config["analysis"] + + # Display main analysis fields + for field_name, field_key in [ + ("Image Format", "image_format"), + ("QueryLoc Format", "queryloc_format"), + ("Video Format", "video_format"), + ("Recursive Search", "recursive_search") + ]: + if field_key in analysis_section: + field_label = self.create_field_label(field_name, analysis_section[field_key]) + self.container_layout.addWidget(field_label) + + # Display analysis_params + if "analysis_params" in analysis_section: + params = analysis_section["analysis_params"] + for field_name, field_key in [ + ("Modalities", "modalities") + ]: + if field_key in params: + field_label = self.create_field_label(field_name, params[field_key]) + self.container_layout.addWidget(field_label) + + def validatePage(self): + """Save the generated configuration to a file""" + if not self.generated_config: + QMessageBox.warning(self, "Error", "No configuration data available") + return False file_dialog = QFileDialog() file_path, _ = file_dialog.getSaveFileName( - wizard, + self, "Save Configuration File", "", "JSON Files (*.json);;All Files (*)" @@ -891,17 +1055,16 @@ def validatePage(self): try: with open(file_path, 'w') as f: - json.dump(config, f, indent=2) + json.dump(self.generated_config, f, indent=2) self.saved_file_path = file_path return True except Exception as e: - self.label.setText(f"Failed to save file:\n{str(e)}") + QMessageBox.warning(self, "Error", f"Failed to save file:\n{str(e)}") return False else: - self.label.setText("No file selected. Please choose a path to save the configuration.") + QMessageBox.warning(self, "Error", "No file selected. Please choose a path to save the configuration.") return False - class ImportEditorPage(QWizardPage): def __init__(self, parent=None): super().__init__(parent) @@ -928,7 +1091,7 @@ def initializePage(self): intro_page = wizard.page(0) if hasattr(intro_page, 'imported_config') and intro_page.imported_config: - with open("config_files/master_JSON.json", "r") as f: + with open("ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: master_json = json.load(f) self.form_widget = build_form_from_template(master_json, intro_page.imported_config) @@ -943,52 +1106,8 @@ def initializePage(self): error_label.setAlignment(Qt.AlignCenter) self.layout.addWidget(error_label) - def validatePage(self): - if not self.form_widget: - QMessageBox.warning(self, "Error", "No configuration to save") - return False - - # Get the master template - with open("config_files/master_JSON.json", "r") as f: - master_json = json.load(f) - - wizard = self.wizard() - intro_page = wizard.page(0) - - # Generate the JSON from the form - config = generate_json(self.form_widget, master_json) - - if not config: - QMessageBox.warning(self, "Error", "No configuration data to save") - return False - - # Show save file dialog - file_dialog = QFileDialog() - file_path, _ = file_dialog.getSaveFileName( - self, - "Save Configuration File", - "", - "JSON Files (*.json);;All Files (*)" - ) - - if file_path: - if not file_path.endswith('.json'): - file_path += '.json' - - try: - with open(file_path, 'w') as f: - json.dump(config, f, indent=2) - self.saved_file_path = file_path - return True - except Exception as e: - QMessageBox.warning(self, "Error", f"Failed to save file:\n{str(e)}") - return False - - QMessageBox.warning(self, "Error", "No file selected") - return False - def nextId(self): - return 8 + return 6 class EndPage(QWizardPage): def __init__(self, parent=None): From ec6f8859e030f5e7fcb129b6bb9ac44ad0d738ce Mon Sep 17 00:00:00 2001 From: Niko Rios Date: Tue, 11 Nov 2025 20:54:08 -0600 Subject: [PATCH 3/4] Updated Master and Advanced JSONs to match FCell meao files. Fixed Review page bugs, --- .idea/vcs.xml | 2 +- ocvl/function/gui/import_generation.py | 19 +- .../advanced_config_JSON.json | 41 +- .../gui/master_config_files/master_JSON.json | 129 +++-- ocvl/function/gui/wizard_creator.py | 467 +++++++++--------- 5 files changed, 345 insertions(+), 313 deletions(-) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/ocvl/function/gui/import_generation.py b/ocvl/function/gui/import_generation.py index fe41516..c77674f 100644 --- a/ocvl/function/gui/import_generation.py +++ b/ocvl/function/gui/import_generation.py @@ -15,10 +15,14 @@ def extract_widget_type(field_def): return field_def.get("type") return field_def -def create_format_editor_widget(config_dict): - """Create FormatEditorWidget with appropriate type based on config""" - format_type = config_dict.get("format_type", "Format") - return FormatEditorWidget(format_type) +def create_format_editor_widget_from_spec(field_key: str, widget_spec: dict): + """Build a FormatEditorWidget with its type coming from the template's 'format_type'.""" + fmt_type = (widget_spec or {}).get("format_type") + # Label shown left of the widget row uses format_label(key) elsewhere. + # The FormatEditorWidget itself shows the format string, so label text isn't critical here. + # We still pass something readable as label_text: + return FormatEditorWidget(label_text=format_label(field_key), default_format="", type=fmt_type) + WIDGET_FACTORY = { # Main fields @@ -39,7 +43,7 @@ def create_format_editor_widget(config_dict): "controlComboBox": lambda config=None: DropdownMenu(default="null", options=["none", "subtraction", "division"]), "listEditor": lambda config=None: ListEditorWidget(), "openFolder": lambda config=None: OpenFolder(), - "formatEditor": lambda config=None: create_format_editor_widget(config or {}), + "formatEditor": lambda key, spec=None: create_format_editor_widget_from_spec(key, spec or {}), "groupbyEditor": lambda config=None: GroupByFormatEditorWidget(None, None, None, "Group By"), "cmapSelector": lambda config=None: ColorMapSelector(), "affineRigidSelector": lambda config=None: AffineRigidSelector(), @@ -120,7 +124,10 @@ def build_form_from_template(template: dict, data: dict, adv=False, parent_name= if not widget_constructor: continue - field_widget = widget_constructor() + if widget_type == "formatEditor": + field_widget = widget_constructor(key, widget_def if isinstance(widget_def, dict) else {}) + else: + field_widget = widget_constructor() if isinstance(field_widget, FormatEditorWidget): field_widget.section_name = parent_name diff --git a/ocvl/function/gui/master_config_files/advanced_config_JSON.json b/ocvl/function/gui/master_config_files/advanced_config_JSON.json index 3967b5e..4703e12 100644 --- a/ocvl/function/gui/master_config_files/advanced_config_JSON.json +++ b/ocvl/function/gui/master_config_files/advanced_config_JSON.json @@ -33,8 +33,8 @@ "dewarp" : "ocvl" }, "trim": { - "start_idx": 0.0, - "end_idx": -1.0 + "start_frm": 0.0, + "end_frm": -1.0 } } }, @@ -60,6 +60,9 @@ "output_folder": null, "output_subfolder": true, "output_subfolder_method": "DateTime", + "output_indiv_stdize_orgs": false, + "output_sum_pop_orgs": false, + "output_sum_indiv_orgs": false, "normalization": { "rescaled": true, "rescale_mean": 70.0 @@ -111,23 +114,33 @@ "height": -1.0 } }, + "debug": { + "output_norm_video": false, + "plot_refine_to_ref": false, + "plot_refine_to_vid": false, + "plot_pop_extracted_orgs": false, + "plot_pop_stdize_orgs": false, + "plot_indiv_stdize_orgs": false, + "plot_valid_cells": false, + "stimulus": true, + "control": true, + "axes": { + "xmin": 0, + "xmax": 6, + "ymin": -255, + "ymax": 255, + "cmap": "viridis", + "legend": true + } + }, "display_params": { - "debug": { - "output_norm_video": false, - "plot_refine_to_ref": false, - "plot_refine_to_vid": false, - "plot_pop_extracted_orgs": false, - "plot_pop_stdize_orgs": false, - "plot_indiv_stdize_orgs": false, - "output_indiv_stdize_orgs": false, - "stimulus": true, - "control": true - }, "pop_summary_overlap": { "stimulus": true, "control": true, "relative": true, "pooled": true, + "cross_group": false, + "annotations": false, "axes": { "xmin": 0, "xmax": 4, @@ -166,6 +179,8 @@ "histogram": true, "cumulative_histogram": true, "map_overlay": true, + "org_video": false, + "cross_group": false, "axes": { "xmin": null, "xstep": null, diff --git a/ocvl/function/gui/master_config_files/master_JSON.json b/ocvl/function/gui/master_config_files/master_JSON.json index 00bdc0d..d925093 100644 --- a/ocvl/function/gui/master_config_files/master_JSON.json +++ b/ocvl/function/gui/master_config_files/master_JSON.json @@ -8,6 +8,7 @@ "raw" : { "video_format": { "type": "formatEditor", + "format_type" : "video", "save": true }, "metadata" : { @@ -15,7 +16,8 @@ "type": "freeText" }, "metadata_format": { - "type": "formatEditor" + "type": "formatEditor", + "format_type" : "meta" }, "fields_to_load": { "timestamps": { @@ -30,14 +32,17 @@ "preanalysis": { "image_format": { "type": "formatEditor", + "format_type" : "image", "save": true }, "video_format": { "type": "formatEditor", + "format_type" : "video", "save": true }, "mask_format": { "type": "formatEditor", + "format_type" : "mask", "save": true }, "metadata": { @@ -45,7 +50,8 @@ "type": "freeText" }, "metadata_format": { - "type": "formatEditor" + "type": "formatEditor", + "format_type" : "meta" }, "fields_to_load": { "framestamps": { @@ -117,10 +123,10 @@ "dewarp" : "freeText" }, "trim": { - "start_idx": { + "start_frm": { "type": "freeNumber" }, - "end_idx": { + "end_frm": { "type": "freeNumber" } } @@ -129,14 +135,17 @@ "analysis": { "image_format": { "type": "formatEditor", + "format_type" : "image", "save": true }, "queryloc_format": { - "type": "formatEditorQueryloc", + "type": "formatEditor", + "format_type" : "queryloc", "save": true }, "video_format": { "type": "formatEditor", + "format_type" : "video", "save": true }, "metadata": { @@ -144,7 +153,8 @@ "type": "freeText" }, "metadata_format": { - "type": "formatEditor" + "type": "formatEditor", + "format_type" : "meta" }, "stimulus_sequence": { "type": "freeText" @@ -184,6 +194,15 @@ "output_subfolder_method": { "type": "outputSubfolderMethodComboBox" }, + "output_indiv_stdize_orgs": { + "type": "trueFalse" + }, + "output_sum_pop_orgs": { + "type": "trueFalse" + }, + "output_sum_indiv_orgs": { + "type": "trueFalse" + }, "normalization": { "method": { "type": "score" @@ -295,56 +314,56 @@ } } }, - "display_params": { - "debug": { - "output_norm_video": { - "type": "trueFalse" - }, - "plot_refine_to_ref": { - "type": "trueFalse" - }, - "plot_refine_to_vid": { - "type": "trueFalse" - }, - "plot_pop_extracted_orgs": { - "type": "trueFalse" + "debug": { + "output_norm_video": { + "type": "trueFalse" + }, + "plot_refine_to_ref": { + "type": "trueFalse" + }, + "plot_refine_to_vid": { + "type": "trueFalse" + }, + "plot_pop_extracted_orgs": { + "type": "trueFalse" + }, + "plot_pop_stdize_orgs": { + "type": "trueFalse" + }, + "plot_indiv_stdize_orgs": { + "type": "trueFalse" + }, + "plot_valid_cells": { + "type": "trueFalse" + }, + "stimulus": { + "type": "trueFalse" + }, + "control": { + "type": "trueFalse" + }, + "axes": { + "xmin": { + "type": "freeNumber" }, - "plot_pop_stdize_orgs": { - "type": "trueFalse" + "xmax": { + "type": "freeNumber" }, - "plot_indiv_stdize_orgs": { - "type": "trueFalse" + "ymin": { + "type": "freeNumber" }, - "output_indiv_stdize_orgs": { - "type": "trueFalse" + "ymax": { + "type": "freeNumber" }, - "stimulus": { - "type": "trueFalse" + "cmap": { + "type": "cmapSelector" }, - "control": { + "legend": { "type": "trueFalse" - }, - "axes": { - "xmin": { - "type": "freeNumber" - }, - "xmax": { - "type": "freeNumber" - }, - "ymin": { - "type": "freeNumber" - }, - "ymax": { - "type": "freeNumber" - }, - "cmap": { - "type": "cmapSelector" - }, - "legend": { - "type": "trueFalse" - } } - }, + } + }, + "display_params": { "pop_summary_overlap": { "stimulus": { "type": "trueFalse" @@ -358,6 +377,12 @@ "pooled": { "type": "trueFalse" }, + "cross_group": { + "type": "trueFalse" + }, + "annotations": { + "type": "trueFalse" + }, "axes": { "xmin": { "type": "freeNumber" @@ -448,6 +473,12 @@ "map_overlay": { "type": "trueFalse" }, + "org_video": { + "type": "trueFalse" + }, + "cross_group": { + "type": "trueFalse" + }, "axes": { "xmin": { "type": "freeNumber" diff --git a/ocvl/function/gui/wizard_creator.py b/ocvl/function/gui/wizard_creator.py index 71e28db..d474b07 100644 --- a/ocvl/function/gui/wizard_creator.py +++ b/ocvl/function/gui/wizard_creator.py @@ -6,7 +6,7 @@ from PySide6.QtWidgets import QWizard import constructors from import_generation import * -from ocvl.function.utility.dataset import parse_metadata +#from ocvl.function.utility.dataset import parse_metadata import tempfile bold = QtGui.QFont() @@ -24,6 +24,16 @@ class TextColor: YELLOW = '\033[93m' RED = '\033[91m' +def _clear_layout(layout): + while layout and layout.count(): + item = layout.takeAt(0) + w = item.widget() + if w is not None: + w.setParent(None) + w.deleteLater() + elif item.layout(): + _clear_layout(item.layout()) + def has_modality(format_string): return "{Modality}" in format_string if format_string else False @@ -739,9 +749,9 @@ def __init__(self, parent=None): 'You can edit any configuration field from here. For a more simple setup, go back and select "Simple Setup"\n' 'Tip: Expand window for better visibility') - with open("ocvl/function/gui/master_config_files/advanced_config_JSON.json", "r") as f: + with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\advanced_config_JSON.json", "r") as f: advanced_config_json = json.load(f) - with open("ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: + with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\master_JSON.json", "r") as f: self.master_json = json.load(f) scroll_area = QScrollArea() @@ -769,290 +779,258 @@ def __init__(self, parent=None): self.saved_file_path = None self.generated_config = None - # Create scrollable area for the review content + # Scroll container self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) - # Container widget for all content self.container = QWidget() self.scroll.setWidget(self.container) - # Main layout self.main_layout = QVBoxLayout(self) self.main_layout.addWidget(self.scroll) - # Container layout self.container_layout = QVBoxLayout(self.container) def nextId(self): return 8 - def initializePage(self): - wizard = self.wizard() + # ---------- helpers: consistent formatting ---------- + def _fmt_value(self, v): + """Human-friendly rendering of values for the review UI.""" + if v is None: + return "null" + if isinstance(v, bool): + return "true" if v else "false" + if isinstance(v, (int, float, str)): + return str(v) + if isinstance(v, (list, tuple)): + # render one item per line if long; inline if short + if len(v) <= 3 and all(isinstance(x, (int, float, str)) for x in v): + return "[" + ", ".join(self._fmt_value(x) for x in v) + "]" + return "\n• " + "\n• ".join(self._fmt_value(x) for x in v) + # fallback to JSON-ish for unusual objects + return str(v) + + def _mk_field(self, title, value): + lab = QLabel(f"{title}: {self._fmt_value(value)}") + lab.setWordWrap(True) + lab.setStyleSheet( + "QLabel { padding: 3px 6px; margin: 2px 0; border-left: 3px solid #007ACC; }" + ) + return lab + + def _mk_section(self, title): + lab = QLabel(title.upper()) + f = lab.font(); + f.setBold(True); + lab.setFont(f) + lab.setStyleSheet("QLabel { font-size: 14px; margin: 10px 0 4px 0; }") + return lab + + def _titleize(self, key): + return str(key).replace("_", " ").title() + + # ---------- schema-walking renderers ---------- + def _render_by_template(self, template_node, data_node, header_name=None): + """ + Render using the 'template' that defined the page (keeps exact key order). + Dict in template -> section; non-dict -> leaf (primitive) field. + """ + if header_name is not None: + self.container_layout.addWidget(self._mk_section(header_name)) + + # If template is a dict: walk keys in order + if isinstance(template_node, dict): + for k, tmpl_child in template_node.items(): + # Skip top-level metadata fields we already printed + if header_name is None and k in ("version", "description"): + continue + + pretty = self._titleize(k) + data_child = None if not isinstance(data_node, dict) else data_node.get(k, None) + + if isinstance(tmpl_child, dict): + # subsection (even if data_child is empty/None, still show header to match UI) + self._render_by_template(tmpl_child, data_child if isinstance(data_child, dict) else {}, pretty) + else: + # leaf: show scalar value taken from data_node (or template default if missing) + value = data_child if data_child is not None else tmpl_child + self.container_layout.addWidget(self._mk_field(pretty, value)) + return + + # If template is not a dict, it's a primitive leaf and should have been handled above. - # Clear any existing labels (except header) - for i in reversed(range(1, self.container_layout.count())): - item = self.container_layout.itemAt(i) - if item and item.widget(): - item.widget().deleteLater() - - # Generate config based on mode - if wizard.page(1).adv_button.isChecked(): - # Advanced mode: use generate_json - advanced_widget = wizard.page(5).advanced_widget - master_json = wizard.page(5).master_json + def _render_pruned_template(self, template_node, data_node): + """ + Make a pruned copy of 'template_node' that only keeps keys present in data_node. + Preserves order from the template. + """ + if not isinstance(template_node, dict) or not isinstance(data_node, dict): + return data_node # primitive (or mismatched) – just return data as-is + + pruned = {} + for k, tmpl_child in template_node.items(): + if k in data_node: + dv = data_node[k] + if isinstance(tmpl_child, dict) and isinstance(dv, dict): + pruned[k] = self._render_pruned_template(tmpl_child, dv) + else: + pruned[k] = tmpl_child + return pruned + + # ---------- SIMPLE ---------- + def _display_simple_mode(self, cfg, wiz): + # (unchanged from your version if you like) – but still benefits from _fmt_value + if "version" in cfg: + self.container_layout.addWidget(self._mk_field("Version", cfg["version"])) + if "description" in cfg: + self.container_layout.addWidget(self._mk_field("Description", cfg["description"])) + + if "preanalysis" in cfg: + pre = cfg["preanalysis"] + self.container_layout.addWidget(self._mk_section("Pre-Analysis")) + for title, k in [("Image Format", "image_format"), + ("Video Format", "video_format"), + ("Mask Format", "mask_format"), + ("Recursive Search", "recursive_search")]: + if k in pre: + self.container_layout.addWidget(self._mk_field(title, pre[k])) + if "pipeline_params" in pre: + params = pre["pipeline_params"] + self.container_layout.addWidget(self._mk_section("Pipeline Parameters")) + for title, k in [("Modalities", "modalities"), + ("Alignment Reference Modality", "alignment_reference_modality"), + ("Group By", "group_by")]: + if k in params: + self.container_layout.addWidget(self._mk_field(title, params[k])) + + if "analysis" in cfg: + ana = cfg["analysis"] + self.container_layout.addWidget(self._mk_section("Analysis")) + for title, k in [("Image Format", "image_format"), + ("Queryloc Format", "queryloc_format"), + ("Video Format", "video_format"), + ("Recursive Search", "recursive_search")]: + if k in ana: + self.container_layout.addWidget(self._mk_field(title, ana[k])) + if "analysis_params" in ana: + params = ana["analysis_params"] + self.container_layout.addWidget(self._mk_section("Analysis Params")) + if "modalities" in params: + self.container_layout.addWidget(self._mk_field("Modalities", params["modalities"])) + + # ---------- ADVANCED ---------- + def _display_advanced_mode(self, cfg, wiz): + # Header fields first + if "version" in cfg: + self.container_layout.addWidget(self._mk_field("Version", cfg["version"])) + if "description" in cfg: + self.container_layout.addWidget(self._mk_field("Description", cfg["description"])) + + # Use the SAME template that built Advanced (preserves visual order) + with open( + r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\advanced_config_JSON.json", + "r") as f: + advanced_template = json.load(f) + + # Render recursively; nested dicts become sections; primitives become leaf rows + self._render_by_template(advanced_template, cfg) + + # ---------- IMPORT ---------- + def _display_import_mode(self, cfg, wiz): + if "version" in cfg: + self.container_layout.addWidget(self._mk_field("Version", cfg["version"])) + if "description" in cfg: + self.container_layout.addWidget(self._mk_field("Description", cfg["description"])) + + # Start from the master template, then prune to only the keys actually present in the imported file + with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\master_JSON.json", + "r") as f: + master_template = json.load(f) + + imported = self.wizard().page(0).imported_config or {} + pruned_template = self._render_pruned_template(master_template, imported) + + self._render_by_template(pruned_template, cfg) + + # ---------- main flow ---------- + def initializePage(self): + wiz = self.wizard() + + # Clear previous render (keep layout) + while self.container_layout.count(): + item = self.container_layout.takeAt(0) + w = item.widget() + if w: + w.deleteLater() + + # Build the config (unchanged logic) + if wiz.page(1).adv_button.isChecked(): + # Advanced + advanced_widget = wiz.page(5).advanced_widget + master_json = wiz.page(5).master_json self.generated_config = generate_json(advanced_widget, master_json) - elif wizard.page(0).import_button.isChecked(): - # Import mode: use generate_json with skip_disabled=False - import_config = wizard.page(7).form_widget - master_json = wizard.page(5).master_json - self.generated_config = generate_json(import_config, master_json, skip_disabled=False) + elif wiz.page(0).import_button.isChecked(): + # Import editor + import_config_widget = wiz.page(7).form_widget + master_json = wiz.page(5).master_json + self.generated_config = generate_json(import_config_widget, master_json, skip_disabled=False) else: - # Simple mode: use the predefined config structure - config = { - "version": wizard.page(2).version_value.text(), - "description": wizard.page(2).description_value.text(), + # Simple (you already build this struct explicitly) + self.generated_config = { + "version": wiz.page(2).version_value.text(), + "description": wiz.page(2).description_value.text(), "preanalysis": { - "image_format": wizard.page(3).image_format_value.get_value(), - "video_format": wizard.page(3).video_format_value.get_value(), - "mask_format": wizard.page(3).mask_format_value.get_value(), - "recursive_search": wizard.page(3).recursive_search_tf.get_value(), + "image_format": wiz.page(3).image_format_value.get_value(), + "video_format": wiz.page(3).video_format_value.get_value(), + "mask_format": wiz.page(3).mask_format_value.get_value(), + "recursive_search": wiz.page(3).recursive_search_tf.get_value(), "pipeline_params": { - "modalities": wizard.page(3).modalities_list_creator.get_list(), - "alignment_reference_modality": wizard.page(3).alignment_ref_value.get_value(), - "group_by": None if wizard.page(3).groupby_value.get_value() == "null" else wizard.page( - 3).groupby_value.get_value(), + "modalities": wiz.page(3).modalities_list_creator.get_list(), + "alignment_reference_modality": wiz.page(3).alignment_ref_value.get_value(), + "group_by": None if wiz.page(3).groupby_value.get_value() == "null" else wiz.page(3).groupby_value.get_value(), } }, "analysis": { - "image_format": wizard.page(4).image_format_value.get_value(), - "queryloc_format": wizard.page(4).queryloc_format_value.get_value(), - "video_format": wizard.page(4).video_format_value.get_value(), - "recursive_search": wizard.page(4).recursive_search_tf.get_value(), + "image_format": wiz.page(4).image_format_value.get_value(), + "queryloc_format": wiz.page(4).queryloc_format_value.get_value(), + "video_format": wiz.page(4).video_format_value.get_value(), + "recursive_search": wiz.page(4).recursive_search_tf.get_value(), "analysis_params": { - "modalities": wizard.page(4).modalities_list_creator.get_list() + "modalities": wiz.page(4).modalities_list_creator.get_list() } } } - self.generated_config = config - - # Parse and display the configuration - self.display_parsed_config() - - def create_field_label(self, field_name, field_value): - """Create a QLabel for a field with name and value""" - label = QLabel(f"{field_name}: {field_value}") - label.setWordWrap(True) - label.setStyleSheet(""" - QLabel { - padding: 5px; - margin: 2px 0; - border-left: 3px solid #007ACC; - } - """) - return label - - def create_section_label(self, section_name): - """Create a bolded section header label""" - label = QLabel(section_name.upper()) - label.setFont(bold) - label.setStyleSheet(""" - QLabel { - font-size: 14px; - font-weight: bold; - margin: 10px 0 5px 0; - padding: 5px; - } - """) - return label - - def display_parsed_config(self): - """Parse the configuration and display results as individual QLabels""" - if not self.generated_config: - error_label = QLabel("No configuration data available") - self.container_layout.addWidget(error_label) - return + # ----- Render by PATH ----- try: - wizard = self.wizard() - is_advanced_mode = wizard.page(1).adv_button.isChecked() - - is_import_mode = wizard.page(0).import_button.isChecked() - - # 1. Display version and description first - if "version" in self.generated_config: - version_label = self.create_field_label("Version", self.generated_config["version"]) - self.container_layout.addWidget(version_label) - - if "description" in self.generated_config: - description_label = self.create_field_label("Description", self.generated_config["description"]) - self.container_layout.addWidget(description_label) - - if is_advanced_mode or is_import_mode: - # Advanced mode: Display all fields dynamically - self.display_all_fields(self.generated_config) + if wiz.page(1).adv_button.isChecked(): + self._display_advanced_mode(self.generated_config, wiz) + elif wiz.page(0).import_button.isChecked(): + self._display_import_mode(self.generated_config, wiz) else: - # Simple mode: Display known fields only - self.display_simple_mode_fields() - - # Add stretch at the end - self.container_layout.addStretch() - + self._display_simple_mode(self.generated_config, wiz) except Exception as e: - error_label = QLabel(f"Error parsing configuration: {str(e)}") - error_label.setStyleSheet("color: red; font-weight: bold;") - self.container_layout.addWidget(error_label) - - def display_all_fields(self, config): - """Display all fields in the configuration recursively for advanced mode""" - # Process sections in the desired order - ordered_sections = ["raw", "preanalysis", "analysis"] - - # First display known sections in order - for section_key in ordered_sections: - if section_key in config: - section_label = self.create_section_label(section_key.replace("_", " ")) - self.container_layout.addWidget(section_label) - - # Try to parse with parse_metadata if possible - try: - parsed_config, parsed_df = parse_metadata(config, root_group=section_key) - except: - pass # Continue if parsing fails - - # Display all fields in this section - self.display_dict_fields(config[section_key]) - - # Then display any additional sections - for key, value in config.items(): - if key not in ["version", "description"] + ordered_sections: - section_label = self.create_section_label(key.replace("_", " ")) - self.container_layout.addWidget(section_label) - - if isinstance(value, dict): - self.display_dict_fields(value) - else: - field_label = self.create_field_label(key.replace("_", " ").title(), value) - self.container_layout.addWidget(field_label) - - def display_dict_fields(self, dictionary, prefix=""): - """Recursively display all fields in a dictionary""" - for key, value in dictionary.items(): - display_key = f"{prefix}{key.replace('_', ' ').title()}" if prefix else key.replace('_', ' ').title() - - if isinstance(value, dict): - # If it's a nested dict, create a sub-section or display with indentation - if len(value) > 3: # Create subsection for large nested dicts - subsection_label = QLabel(f" {display_key}:") - subsection_label.setStyleSheet(""" - QLabel { - font-weight: bold; - margin: 5px 0 2px 10px; - padding: 3px; - } - """) - self.container_layout.addWidget(subsection_label) - self.display_dict_fields(value, " ") - else: - # For small nested dicts, display inline - for sub_key, sub_value in value.items(): - nested_display_key = f"{display_key} - {sub_key.replace('_', ' ').title()}" - field_label = self.create_field_label(nested_display_key, sub_value) - self.container_layout.addWidget(field_label) - else: - # Use the improved create_field_label method for all values - # (it now handles lists, numbers, booleans, etc. internally) - field_label = self.create_field_label(display_key, value) - self.container_layout.addWidget(field_label) - - def display_simple_mode_fields(self): - """Display only the fields configured in simple mode""" - # 2. Display preanalysis section - if "preanalysis" in self.generated_config: - section_label = self.create_section_label("Pre-Analysis") - self.container_layout.addWidget(section_label) - - # Parse with updated function signature - preanalysis_config, preanalysis_df = parse_metadata( - self.generated_config, root_group="preanalysis" - ) - - preanalysis_section = self.generated_config["preanalysis"] - - # Display main preanalysis fields - for field_name, field_key in [ - ("Image Format", "image_format"), - ("Video Format", "video_format"), - ("Mask Format", "mask_format"), - ("Recursive Search", "recursive_search") - ]: - if field_key in preanalysis_section: - field_label = self.create_field_label(field_name, preanalysis_section[field_key]) - self.container_layout.addWidget(field_label) - - # Display pipeline_params - if "pipeline_params" in preanalysis_section: - params = preanalysis_section["pipeline_params"] - for field_name, field_key in [ - ("Modalities", "modalities"), - ("Alignment Reference Modality", "alignment_reference_modality"), - ("Group By", "group_by") - ]: - if field_key in params: - field_label = self.create_field_label(field_name, params[field_key]) - self.container_layout.addWidget(field_label) - - # 3. Display analysis section - if "analysis" in self.generated_config: - section_label = self.create_section_label("Analysis") - self.container_layout.addWidget(section_label) - - # Parse with updated function signature - analysis_config, analysis_df = parse_metadata( - self.generated_config, root_group="analysis" - ) + err = QLabel(f"Error rendering review: {e}") + err.setStyleSheet("color: red; font-weight: bold;") + self.container_layout.addWidget(err) - analysis_section = self.generated_config["analysis"] - - # Display main analysis fields - for field_name, field_key in [ - ("Image Format", "image_format"), - ("QueryLoc Format", "queryloc_format"), - ("Video Format", "video_format"), - ("Recursive Search", "recursive_search") - ]: - if field_key in analysis_section: - field_label = self.create_field_label(field_name, analysis_section[field_key]) - self.container_layout.addWidget(field_label) - - # Display analysis_params - if "analysis_params" in analysis_section: - params = analysis_section["analysis_params"] - for field_name, field_key in [ - ("Modalities", "modalities") - ]: - if field_key in params: - field_label = self.create_field_label(field_name, params[field_key]) - self.container_layout.addWidget(field_label) + self.container_layout.addStretch() def validatePage(self): - """Save the generated configuration to a file""" + """Save the generated configuration to a file (unchanged).""" if not self.generated_config: QMessageBox.warning(self, "Error", "No configuration data available") return False file_dialog = QFileDialog() file_path, _ = file_dialog.getSaveFileName( - self, - "Save Configuration File", - "", - "JSON Files (*.json);;All Files (*)" + self, "Save Configuration File", "", "JSON Files (*.json);;All Files (*)" ) if file_path: if not file_path.endswith('.json'): file_path += '.json' - try: with open(file_path, 'w') as f: json.dump(self.generated_config, f, indent=2) @@ -1065,6 +1043,7 @@ def validatePage(self): QMessageBox.warning(self, "Error", "No file selected. Please choose a path to save the configuration.") return False + class ImportEditorPage(QWizardPage): def __init__(self, parent=None): super().__init__(parent) @@ -1091,7 +1070,7 @@ def initializePage(self): intro_page = wizard.page(0) if hasattr(intro_page, 'imported_config') and intro_page.imported_config: - with open("ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: + with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\master_JSON.json", "r") as f: master_json = json.load(f) self.form_widget = build_form_from_template(master_json, intro_page.imported_config) From c15d0e439a4e7bb773d44f5d32b9f77f9b01f95f Mon Sep 17 00:00:00 2001 From: Niko Rios Date: Fri, 16 Jan 2026 11:51:34 -0600 Subject: [PATCH 4/4] General GUI fixes. --- ocvl/function/gui/constructors.py | 51 ++++++++------- ocvl/function/gui/import_generation.py | 86 ++++++++++++-------------- ocvl/function/gui/wizard_creator.py | 60 +++++++++++------- 3 files changed, 105 insertions(+), 92 deletions(-) diff --git a/ocvl/function/gui/constructors.py b/ocvl/function/gui/constructors.py index 4ee0dc2..90edf55 100644 --- a/ocvl/function/gui/constructors.py +++ b/ocvl/function/gui/constructors.py @@ -324,7 +324,7 @@ class FormatElementsEditor(QDialog): copyRequested = Signal(str, str, str) def __init__(self, current_format=None, parent=None, type=None, section_name=None, format_key=None, - enable_copy=True): + enable_copy=True, show_extensions=True): super().__init__(parent) self.setWindowTitle("Format Editor") self.setGeometry(600, 600, 650, 500) @@ -348,9 +348,13 @@ def __init__(self, current_format=None, parent=None, type=None, section_name=Non self.copy_button = QPushButton("Copy to All in Section") self.copy_button.clicked.connect(self.copy_to_all) - self.file_type_combo = QComboBox() - self.file_type_combo.addItems(self.extension_options) - self.file_type_combo.currentTextChanged.connect(self.update_preview) + # Extension dropdown is optional (GroupBy should not show one) + self.file_type_combo = None + self.show_extensions = show_extensions + if self.show_extensions: + self.file_type_combo = QComboBox() + self.file_type_combo.addItems(self.extension_options) + self.file_type_combo.currentTextChanged.connect(self.update_preview) # === MAIN VERTICAL LAYOUT === window_layout = QVBoxLayout(self) @@ -373,7 +377,8 @@ def __init__(self, current_format=None, parent=None, type=None, section_name=Non preview_layout.addWidget(self.preview_label) preview_layout.addWidget(self.preview_display) - preview_layout.addWidget(self.file_type_combo) + if self.file_type_combo is not None: + preview_layout.addWidget(self.file_type_combo) if enable_copy: preview_layout.addWidget(self.copy_button) preview_layout.setAlignment(Qt.AlignLeft) @@ -541,7 +546,7 @@ def _get_extension_options(self): """Get file extension options based on format type""" if self.type == "image": return [".tif", ".png", ".jpg", ".mat", ".npy"] - elif self.type == "video" or "mask": + elif self.type in ("video", "mask"): return [".avi", ".mov", ".mat", ".npy"] elif self.type == "meta": return [".txt", ".json", ".xml", ".csv", ".log"] @@ -573,13 +578,13 @@ def _detect_existing_extension(self, format_string): def _add_existing_extension_to_dropdown(self, extension): """Add the existing extension to the dropdown if it's not already there""" + if self.file_type_combo is None: + return # No extension dropdown for this editor + if extension and extension not in [self.file_type_combo.itemText(i) for i in range(self.file_type_combo.count())]: - # Add the existing extension at the top of the list self.file_type_combo.insertItem(0, extension) - # Add a separator item for clarity self.file_type_combo.insertSeparator(1) - # Set the existing extension as selected self.file_type_combo.setCurrentText(extension) def copy_to_all(self): @@ -889,7 +894,7 @@ def add_separator(self): self.update_preview() def update_preview(self): - """Update the preview label with the current format string including file extension""" + """Update the preview label with the current format string including file extension (if enabled)""" preview_html = "" for i in range(self.selected_list.count()): @@ -897,17 +902,16 @@ def update_preview(self): item_text = self._get_item_internal_text(item) if item_text.startswith("{Added Text: ") and item_text.endswith("}"): - # Static text - display the actual text separator = item_text[13:-1] preview_html += f"{separator}" else: - # Format element - display with brackets preview_html += f"{item_text}" - # Add the selected file extension only if we don't already have an extension in the format - selected_extension = self.file_type_combo.currentText() - if selected_extension and not self._has_extension_in_format(): - preview_html += selected_extension + # Only append extension if this editor supports extensions + if self.file_type_combo is not None: + selected_extension = self.file_type_combo.currentText() + if selected_extension and not self._has_extension_in_format(): + preview_html += selected_extension self.preview_display.setText(preview_html) @@ -1000,7 +1004,7 @@ def show_context_menu(self, position): self.update_preview() def get_format_string(self): - """Return the complete format string including file extension""" + """Return the complete format string including file extension (if enabled)""" format_string = "" for i in range(self.selected_list.count()): @@ -1008,17 +1012,16 @@ def get_format_string(self): item_text = self._get_item_internal_text(item) if item_text.startswith("{Added Text: ") and item_text.endswith("}"): - # Static text - just add the text part separator = item_text[13:-1] format_string += separator else: - # Format element - add as is format_string += item_text - # Add the selected file extension only if we don't already have one in the format - selected_extension = self.file_type_combo.currentText() - if selected_extension and not self._has_extension_in_format(): - format_string += selected_extension + # Only append extension if this editor supports extensions + if self.file_type_combo is not None: + selected_extension = self.file_type_combo.currentText() + if selected_extension and not self._has_extension_in_format(): + format_string += selected_extension return format_string @@ -1178,7 +1181,7 @@ def _open_format_editor(self): # If current format is "null", pass empty string to the dialog current_format = None if self.current_format == "null" else self.current_format - dialog = FormatElementsEditor(current_format, self, enable_copy=False) + dialog = FormatElementsEditor(current_format, self, enable_copy=False, show_extensions=False) # Override the dialog's available elements with our dynamic ones dialog.original_elements = self.available_elements.copy() diff --git a/ocvl/function/gui/import_generation.py b/ocvl/function/gui/import_generation.py index c77674f..9101160 100644 --- a/ocvl/function/gui/import_generation.py +++ b/ocvl/function/gui/import_generation.py @@ -298,70 +298,64 @@ def update_modalities_enabled(): update_modalities_enabled() def generate_json(form_container, template, skip_disabled=True): - result = {} - form_layout = form_container.layout() - if not form_layout: - return result - - for i in range(form_layout.count()): - item = form_layout.itemAt(i) - widget = item.widget() - if not widget: - continue - - # Handle collapsible sections (nested objects) - if isinstance(widget, CollapsibleSection): - if skip_disabled and not widget.is_enabled(): - continue - - section_title = widget.title().replace(':', '').replace(' ', '_').lower() - - content_layout = widget.content_area.layout() - if not content_layout: + """ + Build JSON from the form without ever re-parenting layouts. + (Re-parenting was breaking collapsibles after Review -> Back.) + """ + def walk_layout(layout, template_for_layout): + result = {} + if not layout: + return result + + for i in range(layout.count()): + item = layout.itemAt(i) + widget = item.widget() + if not widget: continue - content_widget = QWidget() - content_widget.setLayout(content_layout) + # ---- Collapsible section (nested object) ---- + if isinstance(widget, CollapsibleSection): + if skip_disabled and not widget.is_enabled(): + continue - template_for_section = template.get(section_title, {}) + section_key = widget.title().replace(':', '').replace(' ', '_').lower() + content_layout = widget.content_area.layout() + if not content_layout: + continue - section_data = generate_json(content_widget, template_for_section, skip_disabled) - if section_data: - result[section_title] = section_data - continue + section_template = template_for_layout.get(section_key, {}) + section_data = walk_layout(content_layout, section_template) + if section_data: + result[section_key] = section_data + continue - # Handle regular form rows - if isinstance(widget, QWidget): + # ---- Regular row widget ---- row_layout = widget.layout() if not row_layout or row_layout.count() < 2: continue - # The first item is the label, second is the widget (or OptionalField wrapper) label_widget = row_layout.itemAt(0).widget() field_widget = row_layout.itemAt(1).widget() if not isinstance(label_widget, QLabel): continue - # Get the original key from the label label_text = label_widget.text().replace(':', '') key = label_text.replace(' ', '_').lower() - # Handle OptionalField wrapper if present + # OptionalField wrapper if isinstance(field_widget, OptionalField): if skip_disabled and not field_widget.is_checked(): continue field_widget = field_widget.field_widget - # Get the widget type from template to determine how to get the value - widget_type_def = template.get(key) + widget_type_def = template_for_layout.get(key) widget_type = extract_widget_type(widget_type_def) if widget_type_def else None - # Skip if we don't know how to handle this widget type if not widget_type or not isinstance(widget_type, str) or widget_type not in WIDGET_FACTORY: continue - # Get the value from the widget based on its type + # Pull value value = None if hasattr(field_widget, 'get_value'): value = field_widget.get_value() @@ -369,43 +363,41 @@ def generate_json(form_container, template, skip_disabled=True): value = field_widget.get_text() elif hasattr(field_widget, 'get_list'): value = field_widget.get_list() - elif hasattr(field_widget, 'currentText'): # For QComboBox based widgets + elif hasattr(field_widget, 'currentText'): value = field_widget.currentText() - elif hasattr(field_widget, 'text'): # For QLabel and similar + elif hasattr(field_widget, 'text'): value = field_widget.text() - elif hasattr(field_widget, 'isChecked'): # For checkboxes + elif hasattr(field_widget, 'isChecked'): value = field_widget.isChecked() elif isinstance(field_widget, QLabel): value = field_widget.text() - # Convert string values to appropriate types if needed + # Convert string types where appropriate if value is not None: - # Handle numeric values first if isinstance(value, str): - # Try to convert to int or float if possible try: if '.' in value: value = float(value) else: value = int(value) except (ValueError, TypeError): - # If conversion fails, handle special string cases if value.lower() == "null": value = None elif value.lower() == "true": value = True elif value.lower() == "false": value = False - elif widget_type in ["freeInt"] and isinstance(value, (int, float)): + elif widget_type == "freeInt" and isinstance(value, (int, float)): value = int(value) - elif widget_type in ["freeFloat"] and isinstance(value, (int, float)): + elif widget_type == "freeFloat" and isinstance(value, (int, float)): value = float(value) elif widget_type == "trueFalse": value = bool(value) elif widget_type == "null": value = None - # Always add to result, even if value is None result[key] = value - return result \ No newline at end of file + return result + + return walk_layout(form_container.layout(), template) \ No newline at end of file diff --git a/ocvl/function/gui/wizard_creator.py b/ocvl/function/gui/wizard_creator.py index d474b07..e72a648 100644 --- a/ocvl/function/gui/wizard_creator.py +++ b/ocvl/function/gui/wizard_creator.py @@ -189,30 +189,48 @@ def __init__(self, parent=None): main_layout.addWidget(scroll) main_layout.setContentsMargins(0, 0, 0, 0) - def nextId(self): + def validatePage(self): + """ + Only called when the user clicks Next/Import. + We open the file dialog here (NOT in nextId), so Back navigation + doesn't re-trigger the explorer and Qt doesn't flip Next->Finish. + """ if self.create_button.isChecked(): - return 1 - elif self.import_button.isChecked(): + # Create mode: nothing special to validate + return True - # Show file dialog to select JSON file - file_dialog = QFileDialog() - file_path, _ = file_dialog.getOpenFileName( + if self.import_button.isChecked(): + # Always prompt in import mode when advancing from Intro + file_path, _ = QFileDialog.getOpenFileName( self, "Open Configuration File", "", "JSON Files (*.json);;All Files (*)" ) - if file_path: - try: - with open(file_path, 'r') as f: - self.imported_config = json.load(f) - return 7 # Go to advanced setup page - except Exception as e: - QMessageBox.warning(self, "Error", f"Failed to load file:\n{str(e)}") - return -1 # Stay on current page - return -1 # Stay on current page if no file selected - return 2 + if not file_path: + # User canceled: stay on Intro + return False + + try: + with open(file_path, "r") as f: + self.imported_config = json.load(f) + return True + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load file:\n{str(e)}") + return False + + return True + + def nextId(self): + """ + nextId() must be deterministic and NEVER open dialogs. + """ + if self.create_button.isChecked(): + return 1 + if self.import_button.isChecked(): + return 7 + return 1 class SelectionPage(QWizardPage): def __init__(self, parent=None): @@ -749,9 +767,9 @@ def __init__(self, parent=None): 'You can edit any configuration field from here. For a more simple setup, go back and select "Simple Setup"\n' 'Tip: Expand window for better visibility') - with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\advanced_config_JSON.json", "r") as f: + with open(r"ocvl/function/gui/master_config_files/advanced_config_JSON.json", "r") as f: advanced_config_json = json.load(f) - with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\master_JSON.json", "r") as f: + with open(r"ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: self.master_json = json.load(f) scroll_area = QScrollArea() @@ -929,7 +947,7 @@ def _display_advanced_mode(self, cfg, wiz): # Use the SAME template that built Advanced (preserves visual order) with open( - r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\advanced_config_JSON.json", + r"ocvl/function/gui/master_config_files/advanced_config_JSON.json", "r") as f: advanced_template = json.load(f) @@ -944,7 +962,7 @@ def _display_import_mode(self, cfg, wiz): self.container_layout.addWidget(self._mk_field("Description", cfg["description"])) # Start from the master template, then prune to only the keys actually present in the imported file - with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\master_JSON.json", + with open(r"ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: master_template = json.load(f) @@ -1070,7 +1088,7 @@ def initializePage(self): intro_page = wizard.page(0) if hasattr(intro_page, 'imported_config') and intro_page.imported_config: - with open(r"C:\Users\nikor\Documents\GitHub\F-Cell\ocvl\function\gui\master_config_files\master_JSON.json", "r") as f: + with open(r"ocvl/function/gui/master_config_files/master_JSON.json", "r") as f: master_json = json.load(f) self.form_widget = build_form_from_template(master_json, intro_page.imported_config)