raised issue fixes

This commit is contained in:
2026-03-23 23:44:34 -07:00
parent 9a32c8e301
commit 868a67b44b
2 changed files with 182 additions and 113 deletions
+11 -3
View File
@@ -1,17 +1,25 @@
# Version 1.3.1 # Version 1.4.0
- This is potentially a save-changing release due to changes in how file paths are stored. Please update your project files to ensure compatibility
- It is still possible to load older saves by enabling 'Incompatible Save Bypass' from the Preferences menu, but your mileage may vary
- Added new parameters to the right side of the screen: MAX_SHIFT, T_MIN, T_MAX. Fixes [Issue 69](https://git.research.dezeeuw.ca/tyler/flares/issues/69) - Added new parameters to the right side of the screen: MAX_SHIFT, T_MIN, T_MAX. Fixes [Issue 69](https://git.research.dezeeuw.ca/tyler/flares/issues/69)
- Added feedback when clicking an analysis option that opens up a new window. Fixes [Issue 20](https://git.research.dezeeuw.ca/tyler/flares/issues/20)
- Fixed an issue where projects can not be saved to a different drive letter on windows. Fixes [Issue 71](https://git.research.dezeeuw.ca/tyler/flares/issues/71)
- Fixed an issue where the fOLD files were not included in the Windows version. Fixes [Issue 60](https://git.research.dezeeuw.ca/tyler/flares/issues/60) - Fixed an issue where the fOLD files were not included in the Windows version. Fixes [Issue 60](https://git.research.dezeeuw.ca/tyler/flares/issues/60)
- Fixed an issue where the MacOS version would fail to persorm some analysis options. Fixes [Issue 63](https://git.research.dezeeuw.ca/tyler/flares/issues/63) - Fixed an issue where the MacOS version would fail to persorm some analysis options. Fixes [Issue 63](https://git.research.dezeeuw.ca/tyler/flares/issues/63)
- Fixed an issue where processing too many participants would cause the analysis button to not appear. Fixes [Issue 61](https://git.research.dezeeuw.ca/tyler/flares/issues/61) - Fixed an issue where processing too many participants would cause the analysis button to not appear. Fixes [Issue 61](https://git.research.dezeeuw.ca/tyler/flares/issues/61)
- Fixed an issue where the error message when a participant fails would not appear. Fixes [Issue 68](https://git.research.dezeeuw.ca/tyler/flares/issues/68) - Fixed an issue where the error message when a participant fails would not appear. Fixes [Issue 68](https://git.research.dezeeuw.ca/tyler/flares/issues/68)
- Fixed an issue where pressing the 'Clear' button after loading a save would cause the application to crash. Fixes [Issue 67](https://git.research.dezeeuw.ca/tyler/flares/issues/67) - Fixed an issue where pressing the 'Clear' button after loading a save would cause the application to crash. Fixes [Issue 67](https://git.research.dezeeuw.ca/tyler/flares/issues/67)
- Fixed an issue where group dropdowns in the Cross-Group viewer would not be updated correctly based on the other groups selected value. Fixes [Issue 49](https://git.research.dezeeuw.ca/tyler/flares/issues/49)
- Fixed an issue where scrollbars were still present after clearing all data. Fixes [Issue 70](https://git.research.dezeeuw.ca/tyler/flares/issues/70)
- Fixed an issue where 'Missing Events Bypass' did not work on the Cross-Group viewer. Fixes [Issue 64](https://git.research.dezeeuw.ca/tyler/flares/issues/64)
- Fixed an issue where bubbles loaded from a save would not resize correctly. Fixes [Issue 14](https://git.research.dezeeuw.ca/tyler/flares/issues/14)
# Version 1.3.0 # Version 1.3.0
- This is a save-changing release due to a new save file format. Please update your project files to ensure compatibility - This is potentially a save-changing release due to a new parameter being saved. Please update your project files to ensure compatibility
- It is still potentially possible to load older saves by enabling 'Incompatible Save Bypass' from the Preferences menu - It is still possible to load older saves by enabling 'Incompatible Save Bypass' from the Preferences menu, but your mileage may vary
- Fixed workers not releasing memory when processing multiple participants. Fixes [Issue 55](https://git.research.dezeeuw.ca/tyler/flares/issues/55) - Fixed workers not releasing memory when processing multiple participants. Fixes [Issue 55](https://git.research.dezeeuw.ca/tyler/flares/issues/55)
- Fixed part of an issue where memory could increase over time despite clicking the clear button. There is still some edge cases where this can occur - Fixed part of an issue where memory could increase over time despite clicking the clear button. There is still some edge cases where this can occur
- Fixed an issue when clearing a bubble, reloading the same file, and clicking it again would cause the app to crash. Fixes [Issue 57](https://git.research.dezeeuw.ca/tyler/flares/issues/57) - Fixed an issue when clearing a bubble, reloading the same file, and clicking it again would cause the app to crash. Fixes [Issue 57](https://git.research.dezeeuw.ca/tyler/flares/issues/57)
+171 -110
View File
@@ -2263,14 +2263,36 @@ class FlaresBaseWidget(QWidget):
return combo return combo
def _get_checked_items(self, combo): # def _get_checked_items(self, combo):
model = combo.model() # model = combo.model()
checked = [] # checked = []
for i in range(2, model.rowCount()): # Start at 2 to skip dummy/toggle # for i in range(2, model.rowCount()): # Start at 2 to skip dummy/toggle
item = model.item(i) # item = model.item(i)
if item.checkState() == Qt.Checked: # if item.checkState() == Qt.Checked:
checked.append(item.text()) # checked.append(item.text())
return checked # return checked
def _get_checked_items(self, combo=None):
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"}
for row in range(model.rowCount()):
item = model.item(row)
if item.checkState() == Qt.CheckState.Checked:
text = item.text()
# 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
def update_participant_dropdown_label(self, combo=None): def update_participant_dropdown_label(self, combo=None):
@@ -2308,7 +2330,6 @@ class FlaresBaseWidget(QWidget):
self._update_event_dropdown() self._update_event_dropdown()
def update_image_index_dropdown_label(self): def update_image_index_dropdown_label(self):
selected = self._get_checked_items(self.image_index_dropdown) selected = self._get_checked_items(self.image_index_dropdown)
if not selected: if not selected:
@@ -2320,51 +2341,95 @@ class FlaresBaseWidget(QWidget):
def _update_event_dropdown(self): def _update_event_dropdown(self):
selected_display_names = self._get_checked_items(self.participant_dropdown) is_split_group = hasattr(self, 'participant_dropdown_a') and hasattr(self, 'participant_dropdown_b')
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("<None Selected>")
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("<None Selected>")
return
bypass = False bypass = False
main_win = next((w for w in QApplication.topLevelWidgets() main_win = next((w for w in QApplication.topLevelWidgets()
if w.objectName() == "MainApplication" or hasattr(w, "missing_events_bypass")), None) if w.objectName() == "MainApplication" or hasattr(w, "missing_events_bypass")), None)
if main_win: if main_win:
bypass = main_win.missing_events_bypass bypass = getattr(main_win, "missing_events_bypass", False)
if is_split_group:
names_a = self._get_checked_items(self.participant_dropdown_a)
names_b = self._get_checked_items(self.participant_dropdown_b)
if not names_a or not names_b:
self._clear_event_dropdown()
return
map_a = getattr(self, 'participant_map_a', {})
rev_a = {f"{l} ({os.path.basename(fp)})": fp for fp, l in map_a.items()}
sets_a = []
for n in names_a:
raw = self.haemo_dict.get(rev_a.get(n))
if raw and hasattr(raw, "annotations"):
sets_a.append(set(raw.annotations.description))
map_b = getattr(self, 'participant_map_b', {})
rev_b = {f"{l} ({os.path.basename(fp)})": fp for fp, l in map_b.items()}
sets_b = []
for n in names_b:
raw = self.haemo_dict.get(rev_b.get(n))
if raw and hasattr(raw, "annotations"):
sets_b.append(set(raw.annotations.description))
if not sets_a or not sets_b:
self._clear_event_dropdown()
return
if not bypass:
final_annotations = set.intersection(*(sets_a + sets_b))
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}
valid_a = set()
for event in all_events_a:
count = sum(1 for s in sets_a if event in s)
if count >= 2:
valid_a.add(event)
valid_b = set()
for event in all_events_b:
count = sum(1 for s in sets_b if event in s)
if count >= 2:
valid_b.add(event)
final_annotations = valid_a.intersection(valid_b)
if bypass:
final_annotations = set.union(*annotation_sets)
else: else:
final_annotations = set.intersection(*annotation_sets) names = self._get_checked_items(self.participant_dropdown)
if not names:
self._clear_event_dropdown()
return
map_single = getattr(self, 'participant_map', {})
rev_single = {f"{l} ({os.path.basename(fp)})": fp for fp, l in map_single.items()}
all_sets = []
for n in names:
raw = self.haemo_dict.get(rev_single.get(n))
if raw and hasattr(raw, "annotations"):
all_sets.append(set(raw.annotations.description))
if not all_sets:
self._clear_event_dropdown()
return
if not bypass:
final_annotations = set.intersection(*all_sets)
else:
final_annotations = set.union(*all_sets)
self.event_dropdown.clear() self.event_dropdown.clear()
self.event_dropdown.addItem("<None Selected>") self.event_dropdown.addItem("<None Selected>")
for ann in sorted(final_annotations): for ann in sorted(final_annotations):
self.event_dropdown.addItem(ann) self.event_dropdown.addItem(ann)
def _clear_event_dropdown(self):
if hasattr(self, 'event_dropdown'):
self.event_dropdown.clear()
self.event_dropdown.addItem("<None Selected>")
def _connect_select_all_toggle(self, toggle_item, model): def _connect_select_all_toggle(self, toggle_item, model):
"""Helper function to connect the Select All functionality.""" """Helper function to connect the Select All functionality."""
@@ -4187,69 +4252,31 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
self.showMaximized() self.showMaximized()
def _update_group_b_options(self): def _update_group_b_options(self):
selected = self.group_a_dropdown.currentText() """Triggered when Group B changes: Update Group A to exclude B's choice"""
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected) 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() self._update_event_dropdown()
group_b = self.group_b_dropdown.currentText()
self.update_participant_list_for_group(group_b, self.participant_dropdown_b)
def _update_group_a_options(self): def _update_group_a_options(self):
selected = self.group_b_dropdown.currentText() """Triggered when Group A changes: Update Group B to exclude A's choice"""
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected) 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() self._update_event_dropdown()
group_a = self.group_a_dropdown.currentText()
self.update_participant_list_for_group(group_a, self.participant_dropdown_a)
def _on_participants_changed(self, item=None): def _on_participants_changed(self, item=None):
self._update_event_dropdown() self._update_event_dropdown()
def _update_event_dropdown(self):
participants_a = self._get_checked_items(self.participant_dropdown_a)
participants_b = self._get_checked_items(self.participant_dropdown_b)
if not participants_a or not participants_b:
self.event_dropdown.clear()
self.event_dropdown.addItem("<None Selected>")
return
selected_file_paths_a = [
fp for display_name in participants_a
for fp, short_label in self.participant_map_a.items()
if display_name == f"{short_label} ({os.path.basename(fp)})"
]
selected_file_paths_b = [
fp for display_name in participants_b
for fp, short_label in self.participant_map_b.items()
if display_name == f"{short_label} ({os.path.basename(fp)})"
]
all_selected_file_paths = set(selected_file_paths_a + selected_file_paths_b)
if not all_selected_file_paths:
self.event_dropdown.clear()
self.event_dropdown.addItem("<None Selected>")
return
annotation_sets = []
for file_path in all_selected_file_paths:
raw = self.haemo_dict.get(file_path)
if raw is None or not hasattr(raw, "annotations"):
continue
annotation_sets.append(set(raw.annotations.description))
if not annotation_sets:
self.event_dropdown.clear()
self.event_dropdown.addItem("<None Selected>")
return
shared_annotations = set.intersection(*annotation_sets)
self.event_dropdown.clear()
self.event_dropdown.addItem("<None Selected>")
for ann in sorted(shared_annotations):
self.event_dropdown.addItem(ann)
def _refresh_group_dropdown(self, dropdown, exclude): def _refresh_group_dropdown(self, dropdown, exclude):
current = dropdown.currentText() current = dropdown.currentText()
dropdown.blockSignals(True) dropdown.blockSignals(True)
@@ -4266,7 +4293,6 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
dropdown.blockSignals(False) dropdown.blockSignals(False)
def _get_file_paths_from_labels(self, labels, group_name): def _get_file_paths_from_labels(self, labels, group_name):
file_paths = [] file_paths = []
@@ -4441,31 +4467,35 @@ class ViewerLauncherWidget(QWidget):
for file_path, config in config_dict.items() for file_path, config in config_dict.items()
} }
def launch(func, btn, *args):
func(*args)
self._trigger_success(btn)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
btn1 = QPushButton("Open Participant Viewer") btn1 = QPushButton("Open Participant Viewer")
btn1.clicked.connect(lambda: self.open_participant_viewer(haemo_dict, fig_bytes_dict)) btn1.clicked.connect(lambda: launch(self.open_participant_viewer, btn1, haemo_dict, fig_bytes_dict))
btn2 = QPushButton("Open Participant Brain Viewer") btn2 = QPushButton("Open Participant Brain Viewer")
btn2.clicked.connect(lambda: self.open_participant_brain_viewer(haemo_dict, cha_dict)) btn2.clicked.connect(lambda: launch(self.open_participant_brain_viewer, btn2, haemo_dict, cha_dict))
btn3 = QPushButton("Open Participant Fold Channels Viewer") btn3 = QPushButton("Open Participant Fold Channels Viewer")
btn3.clicked.connect(lambda: self.open_participant_fold_channels_viewer(haemo_dict, cha_dict)) btn3.clicked.connect(lambda: launch(self.open_participant_fold_channels_viewer, btn3, haemo_dict, cha_dict))
btn7 = QPushButton("Open Functional Connectivity Viewer [BETA]") btn7 = QPushButton("Open Functional Connectivity Viewer [BETA]")
btn7.clicked.connect(lambda: self.open_participant_functional_connectivity_viewer(haemo_dict, epochs_dict)) btn7.clicked.connect(lambda: launch(self.open_participant_functional_connectivity_viewer, btn7, haemo_dict, epochs_dict))
btn8 = QPushButton("Open Group Functional Connectivity Viewer [BETA]") btn8 = QPushButton("Open Group Functional Connectivity Viewer [BETA]")
btn8.clicked.connect(lambda: self.open_group_functional_connectivity_viewer(haemo_dict, group_dict, config_dict)) btn8.clicked.connect(lambda: launch(self.open_group_functional_connectivity_viewer, btn8, haemo_dict, group_dict, config_dict))
btn4 = QPushButton("Open Inter-Group Viewer") btn4 = QPushButton("Open Inter-Group Viewer")
btn4.clicked.connect(lambda: self.open_group_viewer(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group_dict)) btn4.clicked.connect(lambda: launch(self.open_group_viewer, btn4, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group_dict))
btn5 = QPushButton("Open Cross Group Brain Viewer") btn5 = QPushButton("Open Cross Group Brain Viewer")
btn5.clicked.connect(lambda: self.open_group_brain_viewer(haemo_dict, df_ind, design_matrix, group_dict, contrast_results_dict)) btn5.clicked.connect(lambda: launch(self.open_group_brain_viewer, btn5, haemo_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
btn6 = QPushButton("Open Export Data As CSV Viewer") btn6 = QPushButton("Open Export Data As CSV Viewer")
btn6.clicked.connect(lambda: self.open_export_data_as_csv_viewer(haemo_dict, cha_dict, df_ind, design_matrix, group_dict, contrast_results_dict)) btn6.clicked.connect(lambda: launch(self.open_export_data_as_csv_viewer, btn6, haemo_dict, cha_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
layout.addWidget(btn1) layout.addWidget(btn1)
layout.addWidget(btn2) layout.addWidget(btn2)
@@ -4508,6 +4538,20 @@ class ViewerLauncherWidget(QWidget):
self.export_data_as_csv_viewer = ExportDataAsCSVViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict) self.export_data_as_csv_viewer = ExportDataAsCSVViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict)
self.export_data_as_csv_viewer.show() self.export_data_as_csv_viewer.show()
def _trigger_success(self, button):
"""Temporarily adds a green checkmark to the button text."""
original_text = button.text()
button.setText(f"{original_text}")
button.setStyleSheet("color: green; font-weight: bold;")
# Revert after 1 second
QTimer.singleShot(1000, lambda: self._revert_button(button, original_text))
def _revert_button(self, button, original_text):
button.setText(original_text)
button.setStyleSheet("")
class MainApplication(QMainWindow): class MainApplication(QMainWindow):
""" """
@@ -4857,6 +4901,12 @@ class MainApplication(QMainWindow):
pass pass
widget.deleteLater() widget.deleteLater()
self.bubble_layout.setSpacing(0)
self.bubble_layout.setContentsMargins(0, 0, 0, 0)
self.bubble_container.setMinimumSize(0, 0)
self.bubble_container.resize(0, 0)
self.scroll_area.updateGeometry()
# Data Purge # Data Purge
self.bubble_widgets = {} self.bubble_widgets = {}
self.files_results = {} self.files_results = {}
@@ -4885,7 +4935,7 @@ class MainApplication(QMainWindow):
self.statusBar().showMessage("All data has been cleared.") self.statusBar().showMessage("All data has been cleared.")
#NOTE: leave this here for now #NOTE: leave this here for now. needs other parts uncommented to work
# self.check_memory_leak() # self.check_memory_leak()
# self.find_referrers() # self.find_referrers()
@@ -5109,12 +5159,12 @@ class MainApplication(QMainWindow):
project_dir = project_path.parent project_dir = project_path.parent
file_list = [ file_list = [
str(PurePosixPath(os.path.relpath(Path(bubble.file_path).resolve(), project_dir))) self._get_safe_path(bubble.file_path, project_dir)
for bubble in self.bubble_widgets.values() for bubble in self.bubble_widgets.values()
] ]
progress_states = { progress_states = {
str(PurePosixPath(os.path.relpath(Path(bubble.file_path).resolve(), project_dir))): bubble.current_step self._get_safe_path(bubble.file_path, project_dir): bubble.current_step
for bubble in self.bubble_widgets.values() for bubble in self.bubble_widgets.values()
} }
@@ -5122,9 +5172,8 @@ class MainApplication(QMainWindow):
for full_path, meta in self.metadata_cache.items(): for full_path, meta in self.metadata_cache.items():
try: try:
# Resolve to absolute to be safe, then make relative to project_dir # Resolve to absolute to be safe, then make relative to project_dir
abs_path = str(Path(full_path).resolve()) safe_path = self._get_safe_path(full_path, project_dir)
rel_path = str(PurePosixPath(os.path.relpath(abs_path, project_dir))) rel_metadata[safe_path] = meta
rel_metadata[rel_path] = meta
except Exception as e: except Exception as e:
print(f"Metadata conversion failed for {full_path}: {e}") print(f"Metadata conversion failed for {full_path}: {e}")
@@ -5182,6 +5231,18 @@ class MainApplication(QMainWindow):
QMessageBox.critical(self, "Error", f"Failed to save project:\n{e}") QMessageBox.critical(self, "Error", f"Failed to save project:\n{e}")
def _get_safe_path(self, target_path, start_dir):
try:
# Convert both to absolute paths first
target = Path(target_path).resolve()
base = Path(start_dir).resolve()
rel = os.path.relpath(target, base)
return str(PurePosixPath(rel))
except ValueError:
return str(PurePosixPath(target))
def load_project(self): def load_project(self):
filename, _ = QFileDialog.getOpenFileName( filename, _ = QFileDialog.getOpenFileName(
self, "Load Project", "", "FLARE Project (*.flare)" self, "Load Project", "", "FLARE Project (*.flare)"