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 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)
|
||||||
|
|||||||
@@ -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 bypass:
|
if is_split_group:
|
||||||
final_annotations = set.union(*annotation_sets)
|
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:
|
else:
|
||||||
final_annotations = set.intersection(*annotation_sets)
|
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)
|
||||||
|
|
||||||
|
else:
|
||||||
|
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)"
|
||||||
|
|||||||
Reference in New Issue
Block a user