diff --git a/changelog.md b/changelog.md index 971c035..3c91bd7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +# Version 1.2.1 + +- Added a requirements.txt file to ensure compatibility + + # Version 1.2.0 - This is a save-breaking release due to a new save file format. Please update your project files to ensure compatibility. Fixes [Issue 30](https://git.research.dezeeuw.ca/tyler/flares/issues/30) diff --git a/main.py b/main.py index ef1e5a2..8a407c6 100644 --- a/main.py +++ b/main.py @@ -1877,9 +1877,300 @@ class FullClickComboBox(QComboBox): return super().eventFilter(obj, event) -class ParticipantViewerWidget(QWidget): - def __init__(self, haemo_dict, fig_bytes_dict): + + + + +class FlaresBaseWidget(QWidget): + def __init__(self, caller): super().__init__() + self.caller = caller + self.haemo_dict = None + self._updating_checkstates = False + self.participant_map = {} + self.show_all_events = True + + # These will be defined by the children, but we'll + # initialize them as None so the code doesn't crash. + self.participant_dropdown = None + self.event_dropdown = None + self.image_index_dropdown = None + + + def _create_multiselect_dropdown(self, items): + combo = FullClickComboBox() + combo.setView(QListView()) + model = QStandardItemModel() + combo.setModel(model) + combo.setEditable(True) + combo.lineEdit().setReadOnly(True) + combo.lineEdit().setPlaceholderText("Select...") + + # Setup internal items + dummy = QStandardItem("") + dummy.setFlags(Qt.ItemIsEnabled) + model.appendRow(dummy) + + toggle = QStandardItem("Toggle Select All") + toggle.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + toggle.setData(Qt.Unchecked, Qt.CheckStateRole) + model.appendRow(toggle) + + for text in items: + item = QStandardItem(text) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + model.appendRow(item) + + # Handle clicking the view directly + def on_view_clicked(index): + item = model.itemFromIndex(index) + if item.isCheckable(): + new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked + item.setCheckState(new_state) + combo.view().pressed.connect(on_view_clicked) + + # Logic for "Select All" and Signal Propagation + def on_item_changed(item): + if getattr(self, '_updating_checkstates', False): + return + self._updating_checkstates = True + + normal_items = [model.item(i) for i in range(2, model.rowCount())] + + if item == toggle: + state = toggle.checkState() + for i in normal_items: + i.setCheckState(state) + else: + all_checked = all(i.checkState() == Qt.Checked for i in normal_items) + toggle.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) + + # Trigger the widget's update logic via the existing signal + combo.currentIndexChanged.emit(combo.currentIndex()) + self._updating_checkstates = False + + model.itemChanged.connect(on_item_changed) + combo.setInsertPolicy(QComboBox.NoInsert) + return combo + + + def _get_checked_items(self, combo): + model = combo.model() + checked = [] + for i in range(2, model.rowCount()): # Start at 2 to skip dummy/toggle + item = model.item(i) + if item.checkState() == Qt.Checked: + checked.append(item.text()) + return checked + + + def update_participant_dropdown_label(self, combo=None): + """ + Handles label updates for ANY participant dropdown. + If 'combo' is None, it defaults to the standard self.participant_dropdown. + """ + if isinstance(combo, int): + combo = None + + # 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 + + # 2. Get the checked items and format the text + selected = self._get_checked_items(target_combo) + if not selected: + target_combo.lineEdit().setText("") + else: + # Extract just "Participant N" + selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] + target_combo.lineEdit().setText(", ".join(selected_short)) + + # 3. Conditional trigger for event updates + # We only update events if we aren't in one of the excluded viewers + excluded_viewers = { + "ParticipantViewer", + "ParticipantFoldChannels", + "ExportDataAsCSVViewer", + } + + if getattr(self, "caller", None) not in excluded_viewers: + self._update_event_dropdown() + + + + def update_image_index_dropdown_label(self): + selected = self._get_checked_items(self.image_index_dropdown) + if not selected: + self.image_index_dropdown.lineEdit().setText("") + else: + # Only show the index part + index_labels = [s.split(" ")[0] for s in selected] + self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) + + + def _update_event_dropdown(self): + selected_display_names = self._get_checked_items(self.participant_dropdown) + selected_file_paths = [] + for display_name in selected_display_names: + for fp, short_label in self.participant_map.items(): + expected_display = f"{short_label} ({os.path.basename(fp)})" + if display_name == expected_display: + selected_file_paths.append(fp) + break + + if not selected_file_paths: + self.event_dropdown.clear() + self.event_dropdown.addItem("") + return + + annotation_sets = [] + + for file_path in selected_file_paths: + raw = self.haemo_dict.get(file_path) + if raw is None or not hasattr(raw, "annotations"): + continue + annotations = set(raw.annotations.description) + annotation_sets.append(annotations) + + if not annotation_sets: + self.event_dropdown.clear() + self.event_dropdown.addItem("") + return + + bypass = False + main_win = next((w for w in QApplication.topLevelWidgets() + if w.objectName() == "MainApplication" or hasattr(w, "missing_events_bypass")), None) + + if main_win: + bypass = main_win.missing_events_bypass + + if bypass: + final_annotations = set.union(*annotation_sets) + else: + final_annotations = set.intersection(*annotation_sets) + + self.event_dropdown.clear() + self.event_dropdown.addItem("") + for ann in sorted(final_annotations): + self.event_dropdown.addItem(ann) + + + def _connect_select_all_toggle(self, toggle_item, model): + """Helper function to connect the Select All functionality.""" + normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle + + def on_item_changed(item): + if self._updating_checkstates: + return + self._updating_checkstates = True + + if item == toggle_item: + all_checked = all(i.checkState() == Qt.Checked for i in normal_items) + if all_checked: + for i in normal_items: + i.setCheckState(Qt.Unchecked) + toggle_item.setCheckState(Qt.Unchecked) + else: + for i in normal_items: + i.setCheckState(Qt.Checked) + toggle_item.setCheckState(Qt.Checked) + + else: + # When normal items change, update toggle item + all_checked = all(i.checkState() == Qt.Checked for i in normal_items) + toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) + + if hasattr(self, 'participant_dropdown_a') and model == self.participant_dropdown_a.model(): + self.update_participant_dropdown_label(self.participant_dropdown_a) + elif hasattr(self, 'participant_dropdown_b') and model == self.participant_dropdown_b.model(): + self.update_participant_dropdown_label(self.participant_dropdown_b) + + # Update label text immediately after change + if self.participant_dropdown: + self.update_participant_dropdown_label() + + self._updating_checkstates = False + + model.itemChanged.connect(on_item_changed) + + + + def update_participant_list_for_group(self, group_name=None, combo=None): + + target_combo = combo if combo is not None else getattr(self, "participant_dropdown", None) + if not target_combo: + return + + if isinstance(group_name, int) and combo is None: + target_group = self.group_dropdown.currentText() + elif group_name is not None: + target_group = group_name + else: + # If we have no group_name, look up the text from the correct dropdown + if hasattr(self, 'participant_dropdown_a') and target_combo is self.participant_dropdown_a: + target_group = self.group_a_dropdown.currentText() + elif hasattr(self, 'participant_dropdown_b') and target_combo is self.participant_dropdown_b: + target_group = self.group_b_dropdown.currentText() + else: + target_group = self.group_dropdown.currentText() + + if hasattr(self, 'participant_dropdown_a') and target_combo is self.participant_dropdown_a: + self.participant_map_a = {} + active_map = self.participant_map_a + elif hasattr(self, 'participant_dropdown_b') and target_combo is self.participant_dropdown_b: + self.participant_map_b = {} + active_map = self.participant_map_b + else: + self.participant_map = {} + active_map = self.participant_map + + # 4. Refresh the Model + model = target_combo.model() + model.clear() + + for text in ["", "Toggle Select All"]: + item = QStandardItem(str(text)) + if text == "Toggle Select All": + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + toggle_ref = item + else: + item.setFlags(Qt.ItemIsEnabled) + model.appendRow(item) + + # 5. Populate Data + if str(target_group) == "": + target_combo.setEnabled(False) + self.update_participant_dropdown_label(combo=target_combo) + return + + 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)})" + active_map[file_path] = short_label + + item = QStandardItem(display_label) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + model.appendRow(item) + + self._connect_select_all_toggle(toggle_ref, model) + self.update_participant_dropdown_label(combo=target_combo) + + + + + +class ParticipantViewerWidget(FlaresBaseWidget): + def __init__(self, haemo_dict, fig_bytes_dict): + super().__init__("ParticipantViewer") self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.setWindowTitle("FLARES Participant Viewer") self.haemo_dict = haemo_dict @@ -1932,111 +2223,6 @@ class ParticipantViewerWidget(QWidget): self.showMaximized() - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - combo.setInsertPolicy(QComboBox.NoInsert) - - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - elif item == dummy_item: - pass - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if combo == self.participant_dropdown: - self.update_participant_dropdown_label() - elif combo == self.image_index_dropdown: - self.update_image_index_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - combo.setInsertPolicy(QComboBox.NoInsert) - return combo - - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - # Skip dummy and toggle items: - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - self.image_index_dropdown.lineEdit().setText(", ".join(selected)) def show_selected_images(self): @@ -2128,6 +2314,7 @@ class ParticipantViewerWidget(QWidget): QMessageBox.information(self, "Save Complete", f"Images saved to {save_dir.resolve()}") + class ClickableLabel(QLabel): def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap): super().__init__() @@ -2153,9 +2340,9 @@ class ClickableLabel(QLabel): -class ParticipantBrainViewerWidget(QWidget): +class ParticipantBrainViewerWidget(FlaresBaseWidget): def __init__(self, haemo_dict, cha_dict): - super().__init__() + super().__init__("ParticipantBrainViewer") self.setWindowTitle("FLARES Participant Brain Viewer") self.haemo_dict = haemo_dict self.cha_dict = cha_dict @@ -2212,148 +2399,8 @@ class ParticipantBrainViewerWidget(QWidget): self.thumb_size = QSize(280, 180) self.showMaximized() - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - - - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - combo.setInsertPolicy(QComboBox.NoInsert) - - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - elif item == dummy_item: - pass - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if combo == self.participant_dropdown: - self.update_participant_dropdown_label() - elif combo == self.image_index_dropdown: - self.update_image_index_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - combo.setInsertPolicy(QComboBox.NoInsert) - return combo + - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - # Skip dummy and toggle items: - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - self._update_event_dropdown() - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) - - def _update_event_dropdown(self): - selected_display_names = self._get_checked_items(self.participant_dropdown) - selected_file_paths = [] - for display_name in selected_display_names: - for fp, short_label in self.participant_map.items(): - expected_display = f"{short_label} ({os.path.basename(fp)})" - if display_name == expected_display: - selected_file_paths.append(fp) - break - - if not selected_file_paths: - self.event_dropdown.clear() - self.event_dropdown.addItem("") - return - - annotation_sets = [] - - for file_path in selected_file_paths: - raw = self.haemo_dict.get(file_path) - if raw is None or not hasattr(raw, "annotations"): - continue - annotations = set(raw.annotations.description) - annotation_sets.append(annotations) - - if not annotation_sets: - self.event_dropdown.clear() - self.event_dropdown.addItem("") - return - - shared_annotations = set.intersection(*annotation_sets) - self.event_dropdown.clear() - self.event_dropdown.addItem("") - for ann in sorted(shared_annotations): - self.event_dropdown.addItem(ann) def show_brain_images(self): import flares @@ -2441,6 +2488,12 @@ class ParticipantBrainViewerWidget(QWidget): for file_path in selected_file_paths: haemo_obj = self.haemo_dict.get(file_path) + if selected_event: + participant_events = set(haemo_obj.annotations.description) + if selected_event not in participant_events: + print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.") + continue + if haemo_obj is None: raise Exception("How did we get here?") @@ -2480,9 +2533,9 @@ class ParticipantBrainViewerWidget(QWidget): -class ParticipantFunctionalConnectivityWidget(QWidget): +class ParticipantFunctionalConnectivityWidget(FlaresBaseWidget): def __init__(self, haemo_dict, epochs_dict): - super().__init__() + super().__init__("FunctionalConnectivityWidget") self.setWindowTitle("FLARES Functional Connectivity Viewer [BETA]") self.haemo_dict = haemo_dict self.epochs_dict = epochs_dict @@ -2542,148 +2595,6 @@ class ParticipantFunctionalConnectivityWidget(QWidget): self.thumb_size = QSize(280, 180) self.showMaximized() - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - - - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - combo.setInsertPolicy(QComboBox.NoInsert) - - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - elif item == dummy_item: - pass - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if combo == self.participant_dropdown: - self.update_participant_dropdown_label() - elif combo == self.image_index_dropdown: - self.update_image_index_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - combo.setInsertPolicy(QComboBox.NoInsert) - return combo - - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - # Skip dummy and toggle items: - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - self._update_event_dropdown() - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) - - def _update_event_dropdown(self): - selected_display_names = self._get_checked_items(self.participant_dropdown) - selected_file_paths = [] - for display_name in selected_display_names: - for fp, short_label in self.participant_map.items(): - expected_display = f"{short_label} ({os.path.basename(fp)})" - if display_name == expected_display: - selected_file_paths.append(fp) - break - - if not selected_file_paths: - self.event_dropdown.clear() - self.event_dropdown.addItem("") - return - - annotation_sets = [] - - for file_path in selected_file_paths: - raw = self.haemo_dict.get(file_path) - if raw is None or not hasattr(raw, "annotations"): - continue - annotations = set(raw.annotations.description) - annotation_sets.append(annotations) - - if not annotation_sets: - self.event_dropdown.clear() - self.event_dropdown.addItem("") - return - - shared_annotations = set.intersection(*annotation_sets) - self.event_dropdown.clear() - self.event_dropdown.addItem("") - for ann in sorted(shared_annotations): - self.event_dropdown.addItem(ann) def show_brain_images(self): import flares @@ -2791,6 +2702,12 @@ class ParticipantFunctionalConnectivityWidget(QWidget): haemo_obj = self.haemo_dict.get(file_path) epochs_obj = self.epochs_dict.get(file_path) + if selected_event: + participant_events = set(haemo_obj.annotations.description) + if selected_event not in participant_events: + print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.") + continue + if haemo_obj is None: raise Exception("How did we get here?") @@ -2882,9 +2799,9 @@ def single_participant_worker(file_path, raw_data, result_queue, progress_queue) -class ParticipantFoldChannelsWidget(QWidget): +class ParticipantFoldChannelsWidget(FlaresBaseWidget): def __init__(self, haemo_dict, cha_dict): - super().__init__() + super().__init__("ParticipantFoldChannels") self.setWindowTitle("FLARES Participant Fold Channels Viewer") self.haemo_dict = haemo_dict self.cha_dict = cha_dict @@ -2934,112 +2851,6 @@ class ParticipantFoldChannelsWidget(QWidget): self.thumb_size = QSize(280, 180) self.showMaximized() - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - - - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - combo.setInsertPolicy(QComboBox.NoInsert) - - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - elif item == dummy_item: - pass - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if combo == self.participant_dropdown: - self.update_participant_dropdown_label() - elif combo == self.image_index_dropdown: - self.update_image_index_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - combo.setInsertPolicy(QComboBox.NoInsert) - return combo - - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - # Skip dummy and toggle items: - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) def show_fold_images(self): selected_display_names = self._get_checked_items(self.participant_dropdown) @@ -3208,9 +3019,9 @@ class ParticipantFoldChannelsWidget(QWidget): self.grid_layout.addWidget(container, 0, 0) -class ExportDataAsCSVViewerWidget(QWidget): +class ExportDataAsCSVViewerWidget(FlaresBaseWidget): def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict): - super().__init__() + super().__init__("ExportDataAsCSVViewer") self.setWindowTitle("FLARES Export Data As CSV Viewer") self.haemo_dict = haemo_dict self.cha_dict = cha_dict @@ -3265,113 +3076,7 @@ class ExportDataAsCSVViewerWidget(QWidget): self.thumb_size = QSize(280, 180) self.showMaximized() - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - - - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - combo.setInsertPolicy(QComboBox.NoInsert) - - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - elif item == dummy_item: - pass - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if combo == self.participant_dropdown: - self.update_participant_dropdown_label() - elif combo == self.image_index_dropdown: - self.update_image_index_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - combo.setInsertPolicy(QComboBox.NoInsert) - return combo - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - # Skip dummy and toggle items: - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) - def generate_and_save_csv(self): selected_display_names = self._get_checked_items(self.participant_dropdown) @@ -3637,9 +3342,14 @@ class ParameterInputDialog(QDialog): -class GroupViewerWidget(QWidget): + + + + + +class GroupViewerWidget(FlaresBaseWidget): def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group): - super().__init__() + super().__init__("GroupViewer") self.setWindowTitle("FLARES Group Viewer") self.haemo_dict = haemo_dict self.cha = cha @@ -3647,8 +3357,9 @@ class GroupViewerWidget(QWidget): self.design_matrix = design_matrix self.contrast_results = contrast_results self.group = group - - + self.show_all_events = True + self._updating_checkstates = False + # Create mappings: file_path -> participant label and dropdown display text self.participant_map = {} # file_path -> "Participant 1" self.participant_dropdown_items = [] # "Participant 1 (filename)" @@ -3715,225 +3426,8 @@ class GroupViewerWidget(QWidget): self.thumb_size = QSize(280, 180) self.showMaximized() - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - combo.setInsertPolicy(QComboBox.NoInsert) - - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - elif item == dummy_item: - pass - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if combo == self.participant_dropdown: - self.update_participant_dropdown_label() - elif combo == self.image_index_dropdown: - self.update_image_index_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - combo.setInsertPolicy(QComboBox.NoInsert) - return combo - - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - # Skip dummy and toggle items: - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - - def update_participant_list_for_group(self): - selected_group = self.group_dropdown.currentText() - model = self.participant_dropdown.model() - model.clear() - self.participant_map.clear() - - # Add dummy and toggle select all items again - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - if selected_group == "": - # Disable participant dropdown when no group selected - self.participant_dropdown.setEnabled(False) - self.update_participant_dropdown_label() - return - - # Enable participant dropdown since a valid group is selected - self.participant_dropdown.setEnabled(True) - - group_file_paths = self.group_to_paths.get(selected_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)})" - self.participant_map[file_path] = short_label - - item = QStandardItem(display_label) - item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(item) - - self._connect_select_all_toggle(toggle_item, model) - self.update_participant_dropdown_label() - - - def _connect_select_all_toggle(self, toggle_item, model): - """Helper function to connect the Select All functionality.""" - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if self.participant_dropdown: - self.update_participant_dropdown_label() - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - self._update_event_dropdown() - - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) - - - def _update_event_dropdown(self): - selected_display_names = self._get_checked_items(self.participant_dropdown) - selected_file_paths = [] - for display_name in selected_display_names: - for fp, short_label in self.participant_map.items(): - expected_display = f"{short_label} ({os.path.basename(fp)})" - if display_name == expected_display: - selected_file_paths.append(fp) - break - - if not selected_file_paths: - self.event_dropdown.clear() - self.event_dropdown.addItem("") - return - - annotation_sets = [] - - for file_path in selected_file_paths: - raw = self.haemo_dict.get(file_path) - if raw is None or not hasattr(raw, "annotations"): - continue - annotations = set(raw.annotations.description) - annotation_sets.append(annotations) - - if not annotation_sets: - self.event_dropdown.clear() - self.event_dropdown.addItem("") - return - - shared_annotations = set.intersection(*annotation_sets) - self.event_dropdown.clear() - self.event_dropdown.addItem("") - for ann in sorted(shared_annotations): - self.event_dropdown.addItem(ann) - def show_brain_images(self): import flares @@ -3950,6 +3444,17 @@ class GroupViewerWidget(QWidget): selected_file_paths.append(fp) break + if selected_event: + valid_paths = [] + for fp in selected_file_paths: + raw = self.haemo_dict.get(fp) + # Check if this participant actually has the event in their annotations + 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 = [ int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown) ] @@ -4039,6 +3544,12 @@ class GroupViewerWidget(QWidget): for file_path in selected_file_paths: haemo_obj = self.haemo_dict.get(file_path) + if selected_event: + participant_events = set(haemo_obj.annotations.description) + if selected_event not in participant_events: + print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.") + continue + if haemo_obj is None: continue @@ -4121,9 +3632,9 @@ class GroupViewerWidget(QWidget): -class GroupBrainViewerWidget(QWidget): +class GroupBrainViewerWidget(FlaresBaseWidget): def __init__(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict): - super().__init__() + super().__init__("GroupBrainViewer") self.setWindowTitle("Group Brain Viewer") self.haemo_dict = haemo_dict self.df_ind = df_ind @@ -4221,89 +3732,6 @@ class GroupBrainViewerWidget(QWidget): - def update_participant_list_for_group(self, group_name: str, participant_dropdown: FullClickComboBox): - model = participant_dropdown.model() - model.clear() - - # Maintain separate participant maps for A and B to avoid conflicts - if participant_dropdown == self.participant_dropdown_a: - participant_map = self.participant_map_a = {} - else: - participant_map = self.participant_map_b = {} - - # Add dummy and toggle select all items again - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - if group_name == "": - participant_dropdown.setEnabled(False) - self._update_participant_dropdown_label(participant_dropdown) - return - - participant_dropdown.setEnabled(True) - - group_file_paths = self.group_to_paths.get(group_name, []) - 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)})" - participant_map[file_path] = short_label - - item = QStandardItem(display_label) - item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(item) - - self._connect_select_all_toggle(toggle_item, model) - self._update_participant_dropdown_label(participant_dropdown) - - def _update_participant_dropdown_label(self, participant_dropdown): - selected = self._get_checked_items(participant_dropdown) - if not selected: - participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - participant_dropdown.lineEdit().setText(", ".join(selected_short)) - self._update_event_dropdown() - - def _connect_select_all_toggle(self, toggle_item, model): - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - def on_item_changed(item): - if getattr(self, "_updating_checkstates", False): - return - self._updating_checkstates = True - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - else: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text for participant dropdowns - if hasattr(self, 'participant_dropdown_a') and model == self.participant_dropdown_a.model(): - self._update_participant_dropdown_label(self.participant_dropdown_a) - elif hasattr(self, 'participant_dropdown_b') and model == self.participant_dropdown_b.model(): - self._update_participant_dropdown_label(self.participant_dropdown_b) - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - def _on_participants_changed(self, item=None): self._update_event_dropdown() @@ -4370,135 +3798,7 @@ class GroupBrainViewerWidget(QWidget): dropdown.setCurrentIndex(0) # Reset to "" dropdown.blockSignals(False) - def _create_multiselect_dropdown(self, items): - combo = FullClickComboBox() - combo.setView(QListView()) - model = QStandardItemModel() - combo.setModel(model) - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText("Select...") - dummy_item = QStandardItem("") - dummy_item.setFlags(Qt.ItemIsEnabled) - model.appendRow(dummy_item) - - toggle_item = QStandardItem("Toggle Select All") - toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(toggle_item) - - for item in items: - standard_item = QStandardItem(item) - standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) - standard_item.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(standard_item) - - def on_view_clicked(index): - item = model.itemFromIndex(index) - if item.isCheckable(): - new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked - item.setCheckState(new_state) - - combo.view().pressed.connect(on_view_clicked) - - self._updating_checkstates = False - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - normal_items = [model.item(i) for i in range(2, model.rowCount())] - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - for i in normal_items: - i.setCheckState(Qt.Unchecked if all_checked else Qt.Checked) - toggle_item.setCheckState(Qt.Unchecked if all_checked else Qt.Checked) - - toggle_item.setCheckState(Qt.Checked if all(i.checkState() == Qt.Checked for i in normal_items) else Qt.Unchecked) - - self.update_image_index_dropdown_label() - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - return combo - - def _get_checked_items(self, combo): - checked = [] - model = combo.model() - for i in range(model.rowCount()): - item = model.item(i) - if item.text() in ("", "Toggle Select All"): - continue - if item.checkState() == Qt.Checked: - checked.append(item.text()) - return checked - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) - - - - def _connect_select_all_toggle(self, toggle_item, model): - """Helper function to connect the Select All functionality.""" - normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle - - def on_item_changed(item): - if self._updating_checkstates: - return - self._updating_checkstates = True - - if item == toggle_item: - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - if all_checked: - for i in normal_items: - i.setCheckState(Qt.Unchecked) - toggle_item.setCheckState(Qt.Unchecked) - else: - for i in normal_items: - i.setCheckState(Qt.Checked) - toggle_item.setCheckState(Qt.Checked) - - else: - # When normal items change, update toggle item - all_checked = all(i.checkState() == Qt.Checked for i in normal_items) - toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) - - # Update label text immediately after change - if hasattr(self, 'participant_dropdown_a') and model == self.participant_dropdown_a.model(): - self._update_participant_dropdown_label(self.participant_dropdown_a) - elif hasattr(self, 'participant_dropdown_b') and model == self.participant_dropdown_b.model(): - self._update_participant_dropdown_label(self.participant_dropdown_b) - - self._updating_checkstates = False - - model.itemChanged.connect(on_item_changed) - - def update_participant_dropdown_label(self): - selected = self._get_checked_items(self.participant_dropdown) - if not selected: - self.participant_dropdown.lineEdit().setText("") - else: - # Extract just "Participant N" from "Participant N (filename)" - selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected] - self.participant_dropdown.lineEdit().setText(", ".join(selected_short)) - - - def update_image_index_dropdown_label(self): - selected = self._get_checked_items(self.image_index_dropdown) - if not selected: - self.image_index_dropdown.lineEdit().setText("") - else: - # Only show the index part - index_labels = [s.split(" ")[0] for s in selected] - self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) def _get_file_paths_from_labels(self, labels, group_name): file_paths = [] @@ -4760,6 +4060,7 @@ class MainApplication(QMainWindow): self.first_run = True self.is_2d_bypass = False self.incompatible_save_bypass = False + self.missing_events_bypass = False self.files_total = 0 # total number of files to process self.files_done = set() # set of file paths done (success or fail) @@ -5011,7 +4312,8 @@ class MainApplication(QMainWindow): preferences_menu = menu_bar.addMenu("Preferences") preferences_actions = [ ("2D Data Bypass", "", self.is_2d_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg")), - ("Incompatible Save Bypass", "", self.incompatable_save_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg")) + ("Incompatible Save Bypass", "", self.incompatable_save_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg")), + ("Missing Events Bypass", "", self.missing_events_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg")) ] for name, shortcut, slot, icon in preferences_actions: preferences_menu.addAction(make_action(name, shortcut, slot, icon=icon, checkable=True, checked=False)) @@ -5100,6 +4402,9 @@ class MainApplication(QMainWindow): def incompatable_save_bypass_func(self, checked): self.incompatible_save_bypass = checked + def missing_events_bypass_func(self, checked): + self.missing_events_bypass = checked + def about_window(self): if self.about is None or not self.about.isVisible(): self.about = AboutWindow(self) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f7fb80c Binary files /dev/null and b/requirements.txt differ