raised issue fixes
This commit is contained in:
+11
-3
@@ -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 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 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 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 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
|
||||
|
||||
- This is a save-changing release due to a new save file format. 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
|
||||
- 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 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 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)
|
||||
|
||||
@@ -2263,14 +2263,36 @@ class FlaresBaseWidget(QWidget):
|
||||
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 _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 _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):
|
||||
@@ -2308,7 +2330,6 @@ class FlaresBaseWidget(QWidget):
|
||||
self._update_event_dropdown()
|
||||
|
||||
|
||||
|
||||
def update_image_index_dropdown_label(self):
|
||||
selected = self._get_checked_items(self.image_index_dropdown)
|
||||
if not selected:
|
||||
@@ -2320,51 +2341,95 @@ class FlaresBaseWidget(QWidget):
|
||||
|
||||
|
||||
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("<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
|
||||
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)
|
||||
|
||||
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:
|
||||
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.addItem("<None Selected>")
|
||||
for ann in sorted(final_annotations):
|
||||
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):
|
||||
"""Helper function to connect the Select All functionality."""
|
||||
@@ -4187,69 +4252,31 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
|
||||
self.showMaximized()
|
||||
|
||||
def _update_group_b_options(self):
|
||||
selected = self.group_a_dropdown.currentText()
|
||||
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected)
|
||||
"""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()
|
||||
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):
|
||||
selected = self.group_b_dropdown.currentText()
|
||||
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected)
|
||||
"""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()
|
||||
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):
|
||||
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):
|
||||
current = dropdown.currentText()
|
||||
dropdown.blockSignals(True)
|
||||
@@ -4266,7 +4293,6 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
|
||||
dropdown.blockSignals(False)
|
||||
|
||||
|
||||
|
||||
def _get_file_paths_from_labels(self, labels, group_name):
|
||||
file_paths = []
|
||||
|
||||
@@ -4441,31 +4467,35 @@ class ViewerLauncherWidget(QWidget):
|
||||
for file_path, config in config_dict.items()
|
||||
}
|
||||
|
||||
def launch(func, btn, *args):
|
||||
func(*args)
|
||||
self._trigger_success(btn)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
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.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.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.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.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.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.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.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(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.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):
|
||||
"""
|
||||
@@ -4857,6 +4901,12 @@ class MainApplication(QMainWindow):
|
||||
pass
|
||||
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
|
||||
self.bubble_widgets = {}
|
||||
self.files_results = {}
|
||||
@@ -4885,7 +4935,7 @@ class MainApplication(QMainWindow):
|
||||
|
||||
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.find_referrers()
|
||||
|
||||
@@ -5109,12 +5159,12 @@ class MainApplication(QMainWindow):
|
||||
project_dir = project_path.parent
|
||||
|
||||
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()
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -5122,9 +5172,8 @@ class MainApplication(QMainWindow):
|
||||
for full_path, meta in self.metadata_cache.items():
|
||||
try:
|
||||
# Resolve to absolute to be safe, then make relative to project_dir
|
||||
abs_path = str(Path(full_path).resolve())
|
||||
rel_path = str(PurePosixPath(os.path.relpath(abs_path, project_dir)))
|
||||
rel_metadata[rel_path] = meta
|
||||
safe_path = self._get_safe_path(full_path, project_dir)
|
||||
rel_metadata[safe_path] = meta
|
||||
except Exception as 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}")
|
||||
|
||||
|
||||
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):
|
||||
filename, _ = QFileDialog.getOpenFileName(
|
||||
self, "Load Project", "", "FLARE Project (*.flare)"
|
||||
|
||||
Reference in New Issue
Block a user