mainly whitespace, but also some generic qol improvements

This commit is contained in:
tyler committed 2026-10-14 20:17:23 -07:00
1 parent 2a5e3f5606
commit 3a8ffbdf70
26 files changed
+1871 -1224

No files matched your search

+92 -97
View File
@@ -14,11 +14,12 @@ import pandas as pd
from pandas import DataFrame
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QScrollArea, QSizePolicy, QVBoxLayout, QWidget, QFrame, QSpinBox, QFileDialog
from PySide6.QtGui import QPalette, QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator
from PySide6.QtCore import QEvent, QPoint, QSize, QTimer, Qt, Signal
from src.shared.shareddata import APP_NAME, PIPELINE_STAGES
class FullClickComboBox(QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
@@ -28,16 +29,16 @@ class FullClickComboBox(QComboBox):
def eventFilter(self, obj, event):
if obj == self.lineEdit():
if event.type() == QEvent.MouseButtonPress:
return True
if event.type() == QEvent.MouseButtonRelease:
self.showPopup()
return True
return super().eventFilter(obj, event)
class ClickableLabel(QLabel):
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
@@ -47,7 +48,7 @@ class ClickableLabel(QLabel):
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setFixedSize(thumbnail_pixmap.size())
self.setStyleSheet("border: 1px solid gray; margin: 2px;")
def mousePressEvent(self, event):
viewer = QWidget()
viewer.setWindowTitle(f"Expanded View - {APP_NAME.upper()}")
@@ -100,12 +101,12 @@ class ParameterInputDialog(QDialog):
self.scroll.setMaximumHeight(800)
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_content = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_layout.setContentsMargins(10, 10, 10, 10)
self.scroll.setWidget(self.scroll_content)
main_layout.addWidget(self.scroll)
for idx, param_list in params_dict.items():
@@ -123,7 +124,7 @@ class ParameterInputDialog(QDialog):
# Convert options to string just in case they aren't
options = [str(opt) for opt in param_info.get("options", [])]
widget.addItems(options)
# Set default choice if it exists in the options list
default_val = str(param_info.get("default", ""))
if default_val in options:
@@ -231,14 +232,14 @@ class FullClickComboBox(QComboBox):
def eventFilter(self, obj, event):
if obj == self.lineEdit():
if event.type() == QEvent.MouseButtonPress:
return True
if event.type() == QEvent.MouseButtonRelease:
self.showPopup()
return True
return super().eventFilter(obj, event)
@@ -249,32 +250,32 @@ class FilePickerWidget(QWidget):
def __init__(self, default_val="", file_filter="JSON Files (*.json);;All Files (*)", parent=None):
super().__init__(parent)
# Inline layout to hold line edit and button side-by-side
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(5)
self.line_edit = QLineEdit()
self.line_edit.setText(str(default_val))
self.line_edit.setPlaceholderText("Select file path...")
# Forward internal text updates out through our custom component signal
self.line_edit.textChanged.connect(self.textChanged.emit)
self.browse_btn = QPushButton("Browse...")
self.browse_btn.clicked.connect(self.open_file_dialog)
layout.addWidget(self.line_edit)
layout.addWidget(self.browse_btn)
self.file_filter = file_filter
def open_file_dialog(self):
# Open PySide6 native file browser
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Configuration File",
self.line_edit.text(),
self,
"Select Configuration File",
self.line_edit.text(),
self.file_filter
)
if file_path:
@@ -308,7 +309,7 @@ class ParamSection(QWidget):
"""
dirty_state_changed = Signal(bool)
def __init__(self, section_data, global_widgets):
super().__init__()
layout = QVBoxLayout()
@@ -317,7 +318,7 @@ class ParamSection(QWidget):
self.dependencies = []
self.selected_path = None
self.param_rows = []
self.param_rows = []
self.header_widgets = []
self.dirty_params = {}
self._updating_checkstates = False
@@ -344,7 +345,7 @@ class ParamSection(QWidget):
# Build label text and tooltips
label_text = f"⚠️ {param_name}" if is_advanced else param_name
label = QLabel(label_text)
if is_advanced:
label.setToolTip(f"ADVANCED: {help_text}")
else:
@@ -371,19 +372,19 @@ class ParamSection(QWidget):
widget.setCurrentText(str(default_val))
widget.currentTextChanged.connect(lambda _, p=param_name: self.check_if_changed(p))
widget.currentTextChanged.connect(self.notify_global_update)
elif param_type == int or "int" in type_str:
widget = QLineEdit()
widget.setValidator(QIntValidator())
widget.setText(str(default_val))
widget.textChanged.connect(lambda _, p=param_name: self.check_if_changed(p))
elif param_type == float or "float" in type_str:
widget = QLineEdit()
widget.setValidator(QDoubleValidator())
widget.setText(str(default_val))
widget.textChanged.connect(lambda _, p=param_name: self.check_if_changed(p))
elif param_type == list or "list" in type_str:
options = param.get("options", [])
if param.get("exclusive", True):
@@ -406,12 +407,12 @@ class ParamSection(QWidget):
else:
widget.setValue(15)
widget.valueChanged.connect(lambda _, p=param_name: self.check_if_changed(p))
elif param_type == "json_file":
widget = FilePickerWidget(default_val=default_val, file_filter="JSON Files (*.json)")
widget.textChanged.connect(lambda _, p=param_name: self.check_if_changed(p))
else:
else:
widget = QLineEdit()
widget.setText(str(default_val))
widget.textChanged.connect(lambda _, p=param_name: self.check_if_changed(p))
@@ -447,7 +448,7 @@ class ParamSection(QWidget):
def update_theme_colors(self):
self.title_label.style().unpolish(self.title_label)
self.title_label.style().polish(self.title_label)
def is_different(self, val_a, val_b, param_type=None):
"""Compares two parameter values to determine if they differ."""
type_str = str(param_type).lower()
@@ -566,7 +567,7 @@ class ParamSection(QWidget):
normal_items.append(item)
state = Qt.Checked if item.text() in defaults else Qt.Unchecked
item.setCheckState(state)
# Sync toggle select all item
toggle_item = model.item(1)
if toggle_item and normal_items:
@@ -637,7 +638,7 @@ class ParamSection(QWidget):
for hw in self.header_widgets:
hw.setVisible(has_visible_rows)
self.setVisible(has_visible_rows)
def has_any_changes(self):
@@ -663,7 +664,7 @@ class ParamSection(QWidget):
for cond in dep["conditions"]:
parent_name = cond.get("parent_name") or cond.get("parent")
required_val = str(cond.get("depends_value") if "depends_value" in cond else cond.get("value", "True"))
parent_info = self.widgets.get(parent_name)
if not parent_info:
all_conditions_met = False
@@ -745,7 +746,7 @@ class ParamSection(QWidget):
toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked)
self._updating_checkstates = False
if param_name:
self.update_dropdown_label(param_name)
self.check_if_changed(param_name)
@@ -813,7 +814,7 @@ class ParamSection(QWidget):
selected = self._get_checked_items(widget)
widget.lineEdit().setText(", ".join(selected) if selected else "<None Selected>")
# def update_annotation_dropdown_from_loaded_files(self, bubble_widgets, button1):
# file_paths = [bubble.file_path for bubble in bubble_widgets.values()]
# if not file_paths:
@@ -822,12 +823,12 @@ class ParamSection(QWidget):
# # 1. Start the UI immediately
# progress = QProgressDialog("Accessing Workers...", "Cancel", 0, len(file_paths), self)
# progress.setWindowModality(Qt.WindowModality.WindowModal)
# progress.setMinimumDuration(0)
# progress.setMinimumDuration(0)
# progress.setValue(0)
# # Force the UI to draw the window NOW before we start the loop
# progress.show()
# QApplication.processEvents()
# QApplication.processEvents()
# annotation_sets = []
@@ -843,7 +844,7 @@ class ParamSection(QWidget):
# # This call is now nearly instant because the process is already warm
# future = self.file_executor.submit(_extract_annotations, path)
# try:
# labels_list = future.result()
# labels_list = future.result()
# if labels_list:
# annotation_sets.append(set(labels_list))
# except Exception as e:
@@ -870,10 +871,10 @@ class ProgressBubble(QWidget):
file_path (str): Associated file path stored with the bubble.
"""
clicked = Signal(object)
rightClicked = Signal(object, QPoint)
def __init__(self, display_name, file_path):
super().__init__()
@@ -964,7 +965,7 @@ class ProgressBubble(QWidget):
else:
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
def mark_cancelled(self):
for i, rect in enumerate(self.rects):
if i < self.current_step:
@@ -1074,7 +1075,7 @@ class FlaresBaseWidget(QWidget):
model.itemChanged.connect(on_item_changed)
combo.setInsertPolicy(QComboBox.NoInsert)
return combo
# def _get_checked_items(self, combo):
# model = combo.model()
@@ -1084,20 +1085,20 @@ class FlaresBaseWidget(QWidget):
# if item.checkState() == Qt.Checked:
# checked.append(item.text())
# return checked
def _get_checked_items(
self,
combo: QComboBox | None = None
) -> list[str]:
target = combo if combo is not None else getattr(self, 'participant_dropdown', None)
if target is None or target.model() is None:
return []
model = target.model()
checked_items = []
# Exclusion list: any item text that should never be treated as data
forbidden = {"Toggle All", "Select All", "<None Selected>", "Toggle"}
@@ -1108,7 +1109,7 @@ class FlaresBaseWidget(QWidget):
# Only add if it's not a 'UI control' item
if text not in forbidden and not text.startswith("Toggle"):
checked_items.append(text)
return checked_items
@@ -1125,7 +1126,7 @@ class FlaresBaseWidget(QWidget):
# 1. Figure out which dropdown we are talking to
target_combo = combo if combo is not None else getattr(self, "participant_dropdown", None)
if target_combo is None:
return # Safety check: nothing to update
@@ -1141,11 +1142,11 @@ class FlaresBaseWidget(QWidget):
# 3. Conditional trigger for event updates
# We only update events if we aren't in one of the excluded viewers
excluded_viewers = {
"ParticipantImage",
"ParticipantFoldChannels",
"ExportToCSV",
"ParticipantImage",
"ParticipantFoldChannels",
"ExportToCSV",
}
if getattr(self, "caller", None) not in excluded_viewers:
self._update_event_dropdown()
@@ -1162,7 +1163,7 @@ class FlaresBaseWidget(QWidget):
def _update_event_dropdown(self):
is_split_group = hasattr(self, 'participant_dropdown_a') and hasattr(self, 'participant_dropdown_b')
bypass = False
main_win = next((w for w in QApplication.topLevelWidgets()
if w.objectName() == "MainApplication" or hasattr(w, "missing_events_bypass")), None)
@@ -1199,7 +1200,7 @@ class FlaresBaseWidget(QWidget):
if not bypass:
final_annotations = set.intersection(*(sets_a + sets_b))
else:
else:
all_events_a = {event for s in sets_a for event in s}
all_events_b = {event for s in sets_b for event in s}
@@ -1216,7 +1217,7 @@ class FlaresBaseWidget(QWidget):
valid_b.add(event)
final_annotations = valid_a.intersection(valid_b)
else:
names = self._get_checked_items(self.participant_dropdown)
if not names:
@@ -1288,8 +1289,7 @@ class FlaresBaseWidget(QWidget):
self._updating_checkstates = False
model.itemChanged.connect(on_item_changed)
def update_participant_list_for_group(self, group_name=None, combo=None):
@@ -1323,7 +1323,7 @@ class FlaresBaseWidget(QWidget):
# 4. Refresh the Model
model = target_combo.model()
model.clear()
for text in ["<None Selected>", "Toggle Select All"]:
item = QStandardItem(str(text))
if text == "Toggle Select All":
@@ -1343,7 +1343,7 @@ class FlaresBaseWidget(QWidget):
target_combo.setEnabled(True)
# Get file paths (handles target_group as int or str)
group_file_paths = self.group_to_paths.get(target_group, [])
for i, file_path in enumerate(group_file_paths, start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
@@ -1363,8 +1363,8 @@ class InterGroupUIMixin:
participant_map: dict[str, str]
def setup_inter_group_ui(
self,
index_texts: Sequence[str],
self,
index_texts: Sequence[str],
placeholder_text: str = ""
) -> None:
@@ -1394,7 +1394,7 @@ class InterGroupUIMixin:
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.participant_dropdown_a = self._create_multiselect_dropdown([])
line_edit = self.participant_dropdown_a.lineEdit()
assert line_edit is not None, "Dropdown A must be editable to have a lineEdit"
@@ -1453,10 +1453,10 @@ class InterGroupUIMixin:
def _update_group_b_options(self):
"""Triggered when Group B changes: Update Group A to exclude B's choice"""
selected_b = self.group_b_dropdown.currentText()
# Refresh Group A and exclude what was just picked in Group B
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected_b)
# Update the participants for Group B
self.update_participant_list_for_group(selected_b, self.participant_dropdown_b)
self._update_event_dropdown()
@@ -1464,10 +1464,10 @@ class InterGroupUIMixin:
def _update_group_a_options(self):
"""Triggered when Group A changes: Update Group B to exclude A's choice"""
selected_a = self.group_a_dropdown.currentText()
# Refresh Group B and exclude what was just picked in Group A
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected_a)
# Update the participants for Group A
self.update_participant_list_for_group(selected_a, self.participant_dropdown_a)
self._update_event_dropdown()
@@ -1491,8 +1491,6 @@ class InterGroupUIMixin:
dropdown.setCurrentIndex(0) # Reset to "<None Selected>"
dropdown.blockSignals(False)
def _get_file_paths_from_labels(self, labels, group_name):
file_paths = []
@@ -1516,14 +1514,14 @@ class InterGroupUIMixin:
file_paths.append(file_path)
return file_paths
def get_common_request_data(
self,
parameterized_indexes: dict[int, list[dict[str, Any]]],
df_ind_dict: dict[str, DataFrame] | None = None,
contrast_dfs: dict[str, dict[str, Any]] | None = None,
) -> tuple[str | None, list[str], list[str], list[str], list[int], dict[str, Any]] | None:
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
@@ -1562,7 +1560,7 @@ class InterGroupUIMixin:
}
dynamic_rois = []
if df_ind_dict:
roi_set = set()
for fp in all_selected_paths:
@@ -1572,7 +1570,7 @@ class InterGroupUIMixin:
if roi_set:
dynamic_rois = sorted(list(roi_set))
# Fallback to prevent UI crashes if JSON file doesn't exist or is empty
if not dynamic_rois:
dynamic_rois = ["Option 1", "Option 2"]
@@ -1588,7 +1586,7 @@ class InterGroupUIMixin:
# If an event is selected, only keep contrasts containing the event name as a substring.
if selected_event is None or selected_event in contrast_name:
contrast_set.add(contrast_name)
# Sort them cleanly for the UI
dynamic_contrasts = sorted(list(contrast_set))
@@ -1634,15 +1632,15 @@ class InterGroupUIMixin:
selected_indexes,
param_values,
)
class CSVUIMixin:
def setup_csv_ui(
self,
self,
index_texts: Sequence[str]
) -> None:
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
self.participant_dropdown_items: list[str] = [] # "Participant 1 (filename)"
@@ -1659,7 +1657,7 @@ class CSVUIMixin:
self.participant_dropdown: FullClickComboBox = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
self.index_texts = index_texts
self.image_index_dropdown: FullClickComboBox = self._create_multiselect_dropdown(self.index_texts)
@@ -1693,14 +1691,14 @@ class CSVUIMixin:
class IntraGroupUIMixin:
def setup_intra_group_ui(
self,
index_texts: Sequence[str],
self,
index_texts: Sequence[str],
placeholder_text: str = ""
) -> None:
self.show_all_events = True
self._updating_checkstates = False
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
@@ -1771,7 +1769,7 @@ class IntraGroupUIMixin:
df_ind_dict: dict[str, DataFrame] | None = None,
contrast_dfs: dict[str, dict[str, Any]] | None = None,
) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None:
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
@@ -1793,7 +1791,7 @@ class IntraGroupUIMixin:
if raw is not None and hasattr(raw, "annotations"):
if selected_event in raw.annotations.description:
valid_paths.append(fp)
selected_file_paths = valid_paths
selected_indexes = [
@@ -1803,7 +1801,7 @@ class IntraGroupUIMixin:
if not selected_file_paths:
print("No participants selected.")
return
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
@@ -1814,7 +1812,7 @@ class IntraGroupUIMixin:
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
dynamic_rois = []
if df_ind_dict:
roi_set = set()
for fp in selected_file_paths:
@@ -1840,7 +1838,7 @@ class IntraGroupUIMixin:
# If an event is selected, only keep contrasts containing the event name as a substring.
if selected_event is None or selected_event in contrast_name:
contrast_set.add(contrast_name)
# Sort them cleanly for the UI
dynamic_contrasts = sorted(list(contrast_set))
@@ -1868,7 +1866,6 @@ class IntraGroupUIMixin:
param_info["options"] = dynamic_contrasts
param_info["default"] = dynamic_contrasts[0] if dynamic_contrasts else ""
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
@@ -1878,21 +1875,22 @@ class IntraGroupUIMixin:
return
else:
return
return (
selected_event,
selected_file_paths,
selected_indexes,
param_values,
)
class ParticipantUIMixin:
def setup_participant_ui(
self,
self,
index_texts: Sequence[str],
placeholder_text: str = ""
) -> None:
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
@@ -1913,7 +1911,6 @@ class ParticipantUIMixin:
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.index_texts = index_texts
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
@@ -1968,8 +1965,6 @@ class ParticipantUIMixin:
]
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
@@ -1987,10 +1982,10 @@ class ParticipantUIMixin:
return
else:
return
return (
selected_event,
selected_file_paths,
selected_indexes,
param_values,
selected_event,
selected_file_paths,
selected_indexes,
param_values,
)