new saving format again
This commit is contained in:
+3
-1
@@ -1,7 +1,9 @@
|
||||
# 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
|
||||
- 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 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)
|
||||
|
||||
@@ -1812,6 +1812,19 @@ class ParamSection(QWidget):
|
||||
|
||||
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):
|
||||
"""Toggles bold font on the label if the value differs from default."""
|
||||
info = self.widgets.get(param_name)
|
||||
@@ -4581,7 +4594,22 @@ class MainApplication(QMainWindow):
|
||||
self.missing_events_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.files_total = 0 # total number of files to process
|
||||
@@ -4609,9 +4637,6 @@ class MainApplication(QMainWindow):
|
||||
self.last_clicked_bubble = None
|
||||
self.installEventFilter(self)
|
||||
|
||||
self.file_metadata = {}
|
||||
self.current_file = None
|
||||
|
||||
# Start local pending update check thread
|
||||
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)
|
||||
@@ -5088,48 +5113,62 @@ class MainApplication(QMainWindow):
|
||||
|
||||
|
||||
# TODO: Is this needed?
|
||||
def open_multiple_folders_dialog(self):
|
||||
while True:
|
||||
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
||||
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)
|
||||
# def open_multiple_folders_dialog(self):
|
||||
# while True:
|
||||
# folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
||||
# if not folder_path:
|
||||
# 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
|
||||
more = QMessageBox.question(
|
||||
self,
|
||||
"Add Another?",
|
||||
"Do you want to select another folder?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
if more == QMessageBox.StandardButton.No:
|
||||
break
|
||||
# # Ask if the user wants to add another
|
||||
# more = QMessageBox.question(
|
||||
# self,
|
||||
# "Add Another?",
|
||||
# "Do you want to select another folder?",
|
||||
# QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
# )
|
||||
# if more == QMessageBox.StandardButton.No:
|
||||
# break
|
||||
|
||||
self.button1.setVisible(True)
|
||||
# self.button1.setVisible(True)
|
||||
|
||||
|
||||
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
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
@@ -5138,6 +5177,11 @@ class MainApplication(QMainWindow):
|
||||
)
|
||||
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:
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save Project", "", "FLARE Project (*.flare)"
|
||||
@@ -5179,6 +5223,19 @@ class MainApplication(QMainWindow):
|
||||
|
||||
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
|
||||
project_data = {
|
||||
"version": version,
|
||||
@@ -5186,10 +5243,12 @@ class MainApplication(QMainWindow):
|
||||
"progress_states": progress_states,
|
||||
"raw_haemo_dict": self.raw_haemo_dict,
|
||||
"file_metadata": rel_metadata,
|
||||
"file_parameters": rel_file_params,
|
||||
"config_dict": self.config_dict,
|
||||
"epochs_dict": self.epochs_dict,
|
||||
"fig_bytes_dict": self.fig_bytes_dict,
|
||||
"cha_dict": self.cha_dict,
|
||||
"current_ui_params": current_params,
|
||||
"contrast_results_dict": self.contrast_results_dict,
|
||||
"df_ind_dict": self.df_ind_dict,
|
||||
"design_matrix_dict": self.design_matrix_dict,
|
||||
@@ -5257,7 +5316,8 @@ class MainApplication(QMainWindow):
|
||||
# Check for potentially broken saves
|
||||
checks = [
|
||||
("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:
|
||||
@@ -5289,7 +5349,9 @@ class MainApplication(QMainWindow):
|
||||
project_dir = Path(filename).parent
|
||||
|
||||
saved_cache = data.get("file_metadata", {})
|
||||
raw_params = data.get("file_parameters", {})
|
||||
self.metadata_cache = {}
|
||||
self.file_metadata = {}
|
||||
|
||||
for rel_path, meta_content in saved_cache.items():
|
||||
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)
|
||||
|
||||
for file_path, config in self.config_dict.items():
|
||||
# Only store AGE, GENDER, GROUP
|
||||
self.file_metadata[file_path] = {
|
||||
key: str(config.get(key, "")) # convert to str for QLineEdit
|
||||
for key in ["AGE", "GENDER", "GROUP"]
|
||||
for rel_path in data["file_list"]:
|
||||
abs_path = str((project_dir / Path(rel_path)).resolve())
|
||||
|
||||
if rel_path in raw_params:
|
||||
# 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:
|
||||
first_file = next(iter(self.config_dict.keys()))
|
||||
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])
|
||||
if file_list:
|
||||
self.current_file = file_list[0]
|
||||
self.right_column_widget.show()
|
||||
|
||||
# Restore all other constants to the parameter sections
|
||||
first_config = self.config_dict[first_file]
|
||||
self.restore_sections_from_config(first_config)
|
||||
# Update Metadata fields (Age/Gender/Group) for the selected file
|
||||
curr_meta = self.file_metadata.get(self.current_file, {"AGE": "", "GENDER": "", "GROUP": ""})
|
||||
for key, field in self.meta_fields.items():
|
||||
field.setText(curr_meta.get(key, ""))
|
||||
|
||||
# Re-enable buttons
|
||||
# self.button1.setVisible(True)
|
||||
self.button3.setVisible(True)
|
||||
if "current_ui_params" in data:
|
||||
self.restore_sections_from_config(data["current_ui_params"])
|
||||
|
||||
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}")
|
||||
|
||||
@@ -5384,57 +5460,68 @@ class MainApplication(QMainWindow):
|
||||
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):
|
||||
folder_paths = [folder_paths]
|
||||
# # Clear previous bubbles
|
||||
# # while self.bubble_layout.count():
|
||||
# # item = self.bubble_layout.takeAt(0)
|
||||
# # widget = item.widget()
|
||||
# # if widget:
|
||||
# # widget.deleteLater()
|
||||
|
||||
# Clear previous bubbles
|
||||
# while self.bubble_layout.count():
|
||||
# item = self.bubble_layout.takeAt(0)
|
||||
# widget = item.widget()
|
||||
# if widget:
|
||||
# widget.deleteLater()
|
||||
# temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement
|
||||
# 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()
|
||||
|
||||
temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement
|
||||
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
|
||||
|
||||
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
|
||||
if not hasattr(self, 'selected_paths'):
|
||||
self.selected_paths = []
|
||||
# for folder_path in folder_paths:
|
||||
# if not os.path.isdir(folder_path):
|
||||
# continue
|
||||
|
||||
for folder_path in folder_paths:
|
||||
if not os.path.isdir(folder_path):
|
||||
continue
|
||||
# snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
|
||||
|
||||
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)}"
|
||||
bubble = ProgressBubble(display_name, full_path)
|
||||
bubble.set_loading_state(True)
|
||||
bubble.setCursor(Qt.CursorShape.WaitCursor)
|
||||
# self.bubble_widgets[full_path] = bubble
|
||||
|
||||
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:
|
||||
self.selected_paths.append(full_path)
|
||||
# row = index // cols
|
||||
# col = index % cols
|
||||
# self.bubble_layout.addWidget(bubble, row, col)
|
||||
# index += 1
|
||||
|
||||
row = index // cols
|
||||
col = index % cols
|
||||
self.bubble_layout.addWidget(bubble, row, col)
|
||||
index += 1
|
||||
|
||||
self.statusBar().showMessage(f"{index} file(s) loaded from: {', '.join(folder_paths)}")
|
||||
# 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):
|
||||
if not hasattr(self, 'file_executor') or self.file_executor is None:
|
||||
|
||||
Reference in New Issue
Block a user