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/.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/constructors.py b/ocvl/function/gui/constructors.py
index dd548b0..90edf55 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,39 @@ 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, show_extensions=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.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)
+ # 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)
@@ -351,15 +365,20 @@ 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)
+ 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)
@@ -438,7 +457,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 +529,64 @@ 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 in ("video", "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 _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 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())]:
+ self.file_type_combo.insertItem(0, extension)
+ self.file_type_combo.insertSeparator(1)
+ self.file_type_combo.setCurrentText(extension)
+
def copy_to_all(self):
reply = QMessageBox.question(
self,
@@ -529,7 +606,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 +646,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,20 +684,33 @@ 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")
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:
@@ -628,25 +719,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 +747,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 +756,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 +774,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 +791,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 +819,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 +830,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,35 +884,52 @@ 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 (if enabled)"""
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
separator = item_text[13:-1]
preview_html += f"{separator}"
- # preview_html += f"{separator}"
else:
- # Format element - display in dark green with slight italic
preview_html += f"{item_text}"
- # preview_html += f"{item_text}"
- preview_html += ""
- # preview_html += ""
+ # 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)
+ 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 = {
@@ -836,33 +945,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,34 +989,40 @@ 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 (if enabled)"""
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
separator = item_text[13:-1]
format_string += separator
else:
- # Format element - add as is
format_string += item_text
+ # 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
def get_formatted_preview(self):
@@ -931,12 +1044,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 +1070,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)
@@ -1068,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()
@@ -1234,10 +1347,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 +1780,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 +1805,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..9101160 100644
--- a/ocvl/function/gui/import_generation.py
+++ b/ocvl/function/gui/import_generation.py
@@ -15,33 +15,59 @@ def extract_widget_type(field_def):
return field_def.get("type")
return field_def
+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 = {
- "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",
+ # 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(),
+ "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 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(),
+ "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 = {}
@@ -82,30 +108,52 @@ 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)
- field_widget = widget_constructor()
+ if not widget_constructor:
+ continue
+
+ 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 # 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:
@@ -249,72 +297,65 @@ def update_modalities_enabled():
# Initial update
update_modalities_enabled()
-def generate_json(form_container, template):
- 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
+def generate_json(form_container, template, skip_disabled=True):
+ """
+ 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
- # Handle collapsible sections (nested objects)
- if isinstance(widget, CollapsibleSection):
+ # ---- Collapsible section (nested object) ----
if isinstance(widget, CollapsibleSection):
- if not widget.is_enabled(): # Skip disabled sections
+ if skip_disabled and not widget.is_enabled():
continue
- section_title = widget.title().replace(':', '').replace(' ', '_').lower()
-
+ section_key = widget.title().replace(':', '').replace(' ', '_').lower()
content_layout = widget.content_area.layout()
if not content_layout:
continue
- content_widget = QWidget()
- content_widget.setLayout(content_layout)
-
- template_for_section = template.get(section_title, {})
-
- section_data = generate_json(content_widget, template_for_section)
+ section_template = template_for_layout.get(section_key, {})
+ section_data = walk_layout(content_layout, section_template)
if section_data:
- result[section_title] = 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 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
- 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()
@@ -322,34 +363,41 @@ 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'):
+ value = field_widget.currentText()
+ elif hasattr(field_widget, 'text'):
+ value = field_widget.text()
+ 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:
- if widget_type in ["freeNumber"]:
+ if isinstance(value, str):
try:
- if '.' in str(value):
+ if '.' in value:
value = float(value)
else:
value = int(value)
- except ValueError:
- pass # Keep as string if conversion fails
+ except (ValueError, TypeError):
+ if value.lower() == "null":
+ value = None
+ elif value.lower() == "true":
+ value = True
+ elif value.lower() == "false":
+ value = False
+ elif widget_type == "freeInt" and isinstance(value, (int, float)):
+ value = int(value)
+ 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
- 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)
+
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/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 35d3c96..d925093 100644
--- a/ocvl/function/gui/master_config_files/master_JSON.json
+++ b/ocvl/function/gui/master_config_files/master_JSON.json
@@ -5,17 +5,44 @@
"description": {
"type": "freeText"
},
+ "raw" : {
+ "video_format": {
+ "type": "formatEditor",
+ "format_type" : "video",
+ "save": true
+ },
+ "metadata" : {
+ "type": {
+ "type": "freeText"
+ },
+ "metadata_format": {
+ "type": "formatEditor",
+ "format_type" : "meta"
+ },
+ "fields_to_load": {
+ "timestamps": {
+ "type": "freeText"
+ },
+ "stimulus_train": {
+ "type": "freeText"
+ }
+ }
+ }
+ },
"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": {
@@ -23,7 +50,8 @@
"type": "freeText"
},
"metadata_format": {
- "type": "formatEditor"
+ "type": "formatEditor",
+ "format_type" : "meta"
},
"fields_to_load": {
"framestamps": {
@@ -95,10 +123,10 @@
"dewarp" : "freeText"
},
"trim": {
- "start_idx": {
+ "start_frm": {
"type": "freeNumber"
},
- "end_idx": {
+ "end_frm": {
"type": "freeNumber"
}
}
@@ -107,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": {
@@ -122,7 +153,8 @@
"type": "freeText"
},
"metadata_format": {
- "type": "formatEditor"
+ "type": "formatEditor",
+ "format_type" : "meta"
},
"stimulus_sequence": {
"type": "freeText"
@@ -162,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"
@@ -273,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"
@@ -336,6 +377,12 @@
"pooled": {
"type": "trueFalse"
},
+ "cross_group": {
+ "type": "trueFalse"
+ },
+ "annotations": {
+ "type": "trueFalse"
+ },
"axes": {
"xmin": {
"type": "freeNumber"
@@ -426,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 d558fd7..e72a648 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)
@@ -28,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
@@ -84,8 +90,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 >")
@@ -102,8 +110,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 +134,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)
@@ -154,37 +189,55 @@ 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):
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 +251,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 +332,7 @@ def __init__(self, parent=None):
center_layout.addLayout(button_layout)
+
# Add to outer layout
outer_layout.addLayout(center_layout)
@@ -370,7 +480,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 +489,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 +498,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 +670,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 +678,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 +686,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 +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("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("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()
@@ -687,134 +797,268 @@ 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)
-
- self.label4 = QLabel()
- self.label4.setWordWrap(True)
+ # Scroll container
+ self.scroll = QScrollArea()
+ self.scroll.setWidgetResizable(True)
- self.label5 = QLabel()
- self.label5.setWordWrap(True)
+ self.container = QWidget()
+ self.scroll.setWidget(self.container)
- 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)
+ self.main_layout = QVBoxLayout(self)
+ self.main_layout.addWidget(self.scroll)
- self.layout.addStretch()
+ self.container_layout = QVBoxLayout(self.container)
def nextId(self):
return 8
- 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
+ # ---------- 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
- # 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()
+ # If template is not a dict, it's a primitive leaf and should have been handled above.
+
+ 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"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"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 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 (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": 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": 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": 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": wiz.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()
+ # ----- Render by PATH -----
+ try:
+ 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:
+ self._display_simple_mode(self.generated_config, wiz)
+ except Exception as e:
+ err = QLabel(f"Error rendering review: {e}")
+ err.setStyleSheet("color: red; font-weight: bold;")
+ self.container_layout.addWidget(err)
- if wizard.page(1).adv_button.isChecked():
- # Advanced mode: generate config from widgets directly
- advanced_widget = wizard.page(5).advanced_widget
- master_json = wizard.page(5).master_json
- config = generate_json(advanced_widget, master_json)
- 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
+ self.container_layout.addStretch()
+
+ def validatePage(self):
+ """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(
- wizard,
- "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(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
@@ -844,7 +1088,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(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)
@@ -859,52 +1103,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):