new saving format again

This commit is contained in:
2026-03-24 11:04:14 -07:00
parent 868a67b44b
commit 3a0eaef30d
2 changed files with 182 additions and 93 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
# Version 1.4.0 # 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 - This is potentially a save-changing release due to changes in how file paths and parameters 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 - It is still possible to load older saves by enabling 'Incompatible Save Bypass' from the Preferences menu, but your mileage may vary
- AGE, GENDER, GROUP, loaded files, and all the parameters on the right side of the screen can now be saved before any data has been processed
- If the values fail to load, they will fallback to the previous logic of retreiving these values after processing has occured. Fixes [Issue 66](https://git.research.dezeeuw.ca/tyler/flares/issues/66)
- 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) - 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 projects can not be saved to a different drive letter on windows. Fixes [Issue 71](https://git.research.dezeeuw.ca/tyler/flares/issues/71)
+181 -94
View File
@@ -1812,6 +1812,19 @@ class ParamSection(QWidget):
self.update_dependencies() self.update_dependencies()
def has_any_changes(self):
"""Returns True if any parameter in this section differs from its default."""
for name, info in self.widgets.items():
default = info["default"]
current_val = self.get_param_values().get(name)
if str(current_val) != str(default):
return True
return False
def check_if_changed(self, param_name, current_value): def check_if_changed(self, param_name, current_value):
"""Toggles bold font on the label if the value differs from default.""" """Toggles bold font on the label if the value differs from default."""
info = self.widgets.get(param_name) info = self.widgets.get(param_name)
@@ -4581,7 +4594,22 @@ class MainApplication(QMainWindow):
self.missing_events_bypass = False self.missing_events_bypass = False
self.analysis_clearing_bypass = False self.analysis_clearing_bypass = False
self.metadata_cache = {} # { "file_path": {metadata_dict} }
# Initialization to ensure that saving can occur
self.raw_haemo_dict = {} # Processed Hemodynamic data
self.config_dict = {} # Analysis parameters/settings
self.epochs_dict = {} # Timing/Event data
self.cha_dict = {} # Channel configurations
self.contrast_results_dict = {} # Statistical results
self.df_ind_dict = {} # Individual dataframes
self.design_matrix_dict = {} # GLM Design matrices
self.valid_dict = {} # Quality control/Validity flags
self.fig_bytes_dict = {} # Cached plot images (serialized)
self.file_metadata = {} # AGE, GENDER, GROUP
self.metadata_cache = {} # Internal file/path information metadata cache
self.bubble_widgets = {} # References to the UI "Bubble" objects
self.current_file = None # Tracks the currently selected absolute path
self.metadata_processed.connect(self._safe_ui_update) self.metadata_processed.connect(self._safe_ui_update)
self.files_total = 0 # total number of files to process self.files_total = 0 # total number of files to process
@@ -4609,9 +4637,6 @@ class MainApplication(QMainWindow):
self.last_clicked_bubble = None self.last_clicked_bubble = None
self.installEventFilter(self) self.installEventFilter(self)
self.file_metadata = {}
self.current_file = None
# Start local pending update check thread # Start local pending update check thread
self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, self.platform_suffix, PLATFORM_NAME, APP_NAME) self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, self.platform_suffix, PLATFORM_NAME, APP_NAME)
self.local_check_thread.pending_update_found.connect(self.updater.on_pending_update_found) self.local_check_thread.pending_update_found.connect(self.updater.on_pending_update_found)
@@ -5088,48 +5113,62 @@ class MainApplication(QMainWindow):
# TODO: Is this needed? # TODO: Is this needed?
def open_multiple_folders_dialog(self): # def open_multiple_folders_dialog(self):
while True: # while True:
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder") # folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
if not folder_path: # if not folder_path:
break
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
if not hasattr(self, 'selected_paths'):
self.selected_paths = []
for file_path in snirf_files:
if file_path not in self.selected_paths:
self.selected_paths.append(file_path)
self.show_files_as_bubbles(folder_path)
# for section_widget in self.param_sections:
# if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
# if "REMOVE_EVENTS" in section_widget.widgets:
# section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
# break # break
# else:
# print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget") # snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
# if not hasattr(self, 'selected_paths'):
# self.selected_paths = []
# for file_path in snirf_files:
# if file_path not in self.selected_paths:
# self.selected_paths.append(file_path)
# self.show_files_as_bubbles(folder_path)
# # for section_widget in self.param_sections:
# # if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
# # if "REMOVE_EVENTS" in section_widget.widgets:
# # section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
# # break
# # else:
# # print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget")
# Ask if the user wants to add another # # Ask if the user wants to add another
more = QMessageBox.question( # more = QMessageBox.question(
self, # self,
"Add Another?", # "Add Another?",
"Do you want to select another folder?", # "Do you want to select another folder?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, # QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) # )
if more == QMessageBox.StandardButton.No: # if more == QMessageBox.StandardButton.No:
break # break
self.button1.setVisible(True) # self.button1.setVisible(True)
def save_project(self, onCrash=False): def save_project(self, onCrash=False):
if not getattr(self, 'raw_haemo_dict', None): if hasattr(self, 'current_file') and self.current_file:
self.file_metadata[self.current_file] = {
key: field.text().strip() for key, field in self.meta_fields.items()
}
has_metadata = any(
any(val for val in meta.values())
for meta in self.file_metadata.values()
)
has_param_changes = any(section.has_any_changes() for section in self.param_sections)
# Check if there is processed data
has_processed_data = bool(getattr(self, 'raw_haemo_dict', None))
if not (has_processed_data or has_metadata or has_param_changes):
if not onCrash: # Don't show popups during a crash/autosave if not onCrash: # Don't show popups during a crash/autosave
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -5138,6 +5177,11 @@ class MainApplication(QMainWindow):
) )
return return
if hasattr(self, 'current_file') and self.current_file:
self.file_metadata[self.current_file] = {
key: field.text() for key, field in self.meta_fields.items()
}
if not onCrash: if not onCrash:
filename, _ = QFileDialog.getSaveFileName( filename, _ = QFileDialog.getSaveFileName(
self, "Save Project", "", "FLARE Project (*.flare)" self, "Save Project", "", "FLARE Project (*.flare)"
@@ -5179,6 +5223,19 @@ class MainApplication(QMainWindow):
print(rel_metadata) print(rel_metadata)
rel_file_params = {
self._get_safe_path(f_path, project_dir): meta
for f_path, meta in self.file_metadata.items()
}
current_params = self.get_all_current_ui_params()
# fallback - if UI reading fails, try the first processed file's config
if not current_params and self.config_dict:
first_file = next(iter(self.config_dict.keys()))
current_params = self.config_dict[first_file]
version = CURRENT_VERSION version = CURRENT_VERSION
project_data = { project_data = {
"version": version, "version": version,
@@ -5186,10 +5243,12 @@ class MainApplication(QMainWindow):
"progress_states": progress_states, "progress_states": progress_states,
"raw_haemo_dict": self.raw_haemo_dict, "raw_haemo_dict": self.raw_haemo_dict,
"file_metadata": rel_metadata, "file_metadata": rel_metadata,
"file_parameters": rel_file_params,
"config_dict": self.config_dict, "config_dict": self.config_dict,
"epochs_dict": self.epochs_dict, "epochs_dict": self.epochs_dict,
"fig_bytes_dict": self.fig_bytes_dict, "fig_bytes_dict": self.fig_bytes_dict,
"cha_dict": self.cha_dict, "cha_dict": self.cha_dict,
"current_ui_params": current_params,
"contrast_results_dict": self.contrast_results_dict, "contrast_results_dict": self.contrast_results_dict,
"df_ind_dict": self.df_ind_dict, "df_ind_dict": self.df_ind_dict,
"design_matrix_dict": self.design_matrix_dict, "design_matrix_dict": self.design_matrix_dict,
@@ -5257,7 +5316,8 @@ class MainApplication(QMainWindow):
# Check for potentially broken saves # Check for potentially broken saves
checks = [ checks = [
("version", "<=1.1.7"), ("version", "<=1.1.7"),
("file_metadata", "<=1.2.2") ("file_metadata", "<=1.2.2"),
("file_params", "<=1.3.0")
] ]
for key, ver_str in checks: for key, ver_str in checks:
@@ -5289,7 +5349,9 @@ class MainApplication(QMainWindow):
project_dir = Path(filename).parent project_dir = Path(filename).parent
saved_cache = data.get("file_metadata", {}) saved_cache = data.get("file_metadata", {})
raw_params = data.get("file_parameters", {})
self.metadata_cache = {} self.metadata_cache = {}
self.file_metadata = {}
for rel_path, meta_content in saved_cache.items(): for rel_path, meta_content in saved_cache.items():
abs_path = str((project_dir / Path(rel_path)).resolve()) abs_path = str((project_dir / Path(rel_path)).resolve())
@@ -5307,29 +5369,43 @@ class MainApplication(QMainWindow):
self.show_files_as_bubbles_from_list(file_list, progress_states, filename) self.show_files_as_bubbles_from_list(file_list, progress_states, filename)
for file_path, config in self.config_dict.items(): for rel_path in data["file_list"]:
# Only store AGE, GENDER, GROUP abs_path = str((project_dir / Path(rel_path)).resolve())
self.file_metadata[file_path] = {
key: str(config.get(key, "")) # convert to str for QLineEdit if rel_path in raw_params:
for key in ["AGE", "GENDER", "GROUP"] # Scenario A: New format found
self.file_metadata[abs_path] = raw_params[rel_path]
elif abs_path in self.config_dict:
# Scenario B: Fallback to old config_dict
old_cfg = self.config_dict[abs_path]
self.file_metadata[abs_path] = {
"AGE": str(old_cfg.get("AGE", "")),
"GENDER": str(old_cfg.get("GENDER", "")),
"GROUP": str(old_cfg.get("GROUP", ""))
} }
else:
# Scenario C: Empty default
self.file_metadata[abs_path] = {"AGE": "", "GENDER": "", "GROUP": ""}
if self.config_dict: if file_list:
first_file = next(iter(self.config_dict.keys())) self.current_file = file_list[0]
self.current_file = first_file
# Update meta fields (AGE/GENDER/GROUP)
for key, field in self.meta_fields.items():
field.setText(self.file_metadata[first_file][key])
self.right_column_widget.show() self.right_column_widget.show()
# Restore all other constants to the parameter sections # Update Metadata fields (Age/Gender/Group) for the selected file
first_config = self.config_dict[first_file] curr_meta = self.file_metadata.get(self.current_file, {"AGE": "", "GENDER": "", "GROUP": ""})
self.restore_sections_from_config(first_config) for key, field in self.meta_fields.items():
field.setText(curr_meta.get(key, ""))
# Re-enable buttons if "current_ui_params" in data:
# self.button1.setVisible(True) self.restore_sections_from_config(data["current_ui_params"])
self.button3.setVisible(True)
elif self.config_dict:
first_file = next(iter(self.config_dict.keys()))
self.restore_sections_from_config(self.config_dict[first_file])
has_data = bool(self.raw_haemo_dict)
self.button1.setVisible(not has_data)
self.button3.setVisible(has_data)
QMessageBox.information(self, "Loaded", f"Project loaded from:\n{filename}") QMessageBox.information(self, "Loaded", f"Project loaded from:\n{filename}")
@@ -5384,57 +5460,68 @@ class MainApplication(QMainWindow):
section_widget.update_dependencies() section_widget.update_dependencies()
# def show_files_as_bubbles(self, folder_paths):
def show_files_as_bubbles(self, folder_paths): # if isinstance(folder_paths, str):
# folder_paths = [folder_paths]
if isinstance(folder_paths, str): # # Clear previous bubbles
folder_paths = [folder_paths] # # while self.bubble_layout.count():
# # item = self.bubble_layout.takeAt(0)
# # widget = item.widget()
# # if widget:
# # widget.deleteLater()
# Clear previous bubbles # temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement
# while self.bubble_layout.count(): # temp_bubble.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy. Preferred)
# item = self.bubble_layout.takeAt(0) # # temp_bubble.setAttribute(Qt.WA_OpaquePaintEvent) # Improve rendering?
# widget = item.widget() # temp_bubble.adjustSize() # Adjust size after the widget is created
# if widget: # bubble_width = temp_bubble.width() # Get the actual width of a bubble
# widget.deleteLater() # available_width = self.bubble_container.width()
temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement # cols = max(1, available_width // bubble_width) # Ensure at least 1 column
temp_bubble.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy. Preferred)
# temp_bubble.setAttribute(Qt.WA_OpaquePaintEvent) # Improve rendering?
temp_bubble.adjustSize() # Adjust size after the widget is created
bubble_width = temp_bubble.width() # Get the actual width of a bubble
available_width = self.bubble_container.width()
cols = max(1, available_width // bubble_width) # Ensure at least 1 column # index = 0
# if not hasattr(self, 'selected_paths'):
# self.selected_paths = []
index = 0 # for folder_path in folder_paths:
if not hasattr(self, 'selected_paths'): # if not os.path.isdir(folder_path):
self.selected_paths = [] # continue
for folder_path in folder_paths: # snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
if not os.path.isdir(folder_path):
continue
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")] # for full_path in snirf_files:
for full_path in snirf_files: # display_name = f"{os.path.basename(folder_path)} / {os.path.basename(full_path)}"
# bubble = ProgressBubble(display_name, full_path)
# bubble.set_loading_state(True)
# bubble.setCursor(Qt.CursorShape.WaitCursor)
display_name = f"{os.path.basename(folder_path)} / {os.path.basename(full_path)}" # self.bubble_widgets[full_path] = bubble
bubble = ProgressBubble(display_name, full_path)
bubble.set_loading_state(True)
bubble.setCursor(Qt.CursorShape.WaitCursor)
self.bubble_widgets[full_path] = bubble # if full_path not in self.selected_paths:
# self.selected_paths.append(full_path)
if full_path not in self.selected_paths: # row = index // cols
self.selected_paths.append(full_path) # col = index % cols
# self.bubble_layout.addWidget(bubble, row, col)
# index += 1
row = index // cols # self.statusBar().showMessage(f"{index} file(s) loaded from: {', '.join(folder_paths)}")
col = index % cols
self.bubble_layout.addWidget(bubble, row, col)
index += 1
self.statusBar().showMessage(f"{index} file(s) loaded from: {', '.join(folder_paths)}")
def get_all_current_ui_params(self):
"""Gathers current values from all UI widgets across all sections."""
current_ui_config = {}
try:
for section in self.param_sections:
# This calls the get_param_values() method you shared earlier
section_values = section.get_param_values()
current_ui_config.update(section_values)
return current_ui_config
except Exception as e:
print(f"Error reading UI parameters: {e}")
return None
def show_files_as_bubbles_from_list(self, file_list, progress_states=None, filenames=None): def show_files_as_bubbles_from_list(self, file_list, progress_states=None, filenames=None):
if not hasattr(self, 'file_executor') or self.file_executor is None: if not hasattr(self, 'file_executor') or self.file_executor is None: