release worthy?
This commit is contained in:
572
main.py
572
main.py
@@ -226,6 +226,44 @@ SECTIONS = [
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SaveProjectThread(QThread):
|
||||
finished_signal = Signal(str)
|
||||
error_signal = Signal(str)
|
||||
|
||||
def __init__(self, filename, project_data):
|
||||
super().__init__()
|
||||
self.filename = filename
|
||||
self.project_data = project_data
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import pickle
|
||||
with open(self.filename, "wb") as f:
|
||||
pickle.dump(self.project_data, f)
|
||||
self.finished_signal.emit(self.filename)
|
||||
except Exception as e:
|
||||
self.error_signal.emit(str(e))
|
||||
|
||||
|
||||
class SavingOverlay(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
|
||||
self.setModal(True)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
label = QLabel("Saving Project…")
|
||||
label.setStyleSheet("font-size: 18px; color: white; background-color: rgba(0,0,0,150); padding: 20px; border-radius: 10px;")
|
||||
layout.addWidget(label)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
class TerminalWindow(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
@@ -2439,6 +2477,371 @@ class ParticipantBrainViewerWidget(QWidget):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ParticipantFunctionalConnectivityWidget(QWidget):
|
||||
def __init__(self, haemo_dict, epochs_dict):
|
||||
super().__init__()
|
||||
self.setWindowTitle("FLARES Functional Connectivity Viewer [BETA]")
|
||||
self.haemo_dict = haemo_dict
|
||||
self.epochs_dict = epochs_dict
|
||||
|
||||
QMessageBox.warning(self, "Warning - FLARES", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
||||
"By clicking OK, you accept that the images generated may not be factual.")
|
||||
|
||||
# Create mappings: file_path -> participant label and dropdown display text
|
||||
self.participant_map = {} # file_path -> "Participant 1"
|
||||
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
||||
|
||||
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
||||
short_label = f"Participant {i}"
|
||||
display_label = f"{short_label} ({os.path.basename(file_path)})"
|
||||
self.participant_map[file_path] = short_label
|
||||
self.participant_dropdown_items.append(display_label)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.top_bar = QHBoxLayout()
|
||||
self.layout.addLayout(self.top_bar)
|
||||
|
||||
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
|
||||
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
|
||||
|
||||
self.event_dropdown = QComboBox()
|
||||
self.event_dropdown.addItem("<None Selected>")
|
||||
|
||||
|
||||
self.index_texts = [
|
||||
"0 (Spectral Connectivity Epochs)",
|
||||
"1 (Envelope Correlation)",
|
||||
"2 (Betas)",
|
||||
"3 (Spectral Connectivity Epochs)",
|
||||
]
|
||||
|
||||
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
|
||||
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
|
||||
|
||||
self.submit_button = QPushButton("Submit")
|
||||
self.submit_button.clicked.connect(self.show_brain_images)
|
||||
|
||||
self.top_bar.addWidget(QLabel("Participants:"))
|
||||
self.top_bar.addWidget(self.participant_dropdown)
|
||||
self.top_bar.addWidget(QLabel("Event:"))
|
||||
self.top_bar.addWidget(self.event_dropdown)
|
||||
self.top_bar.addWidget(QLabel("Image Indexes:"))
|
||||
self.top_bar.addWidget(self.image_index_dropdown)
|
||||
self.top_bar.addWidget(self.submit_button)
|
||||
|
||||
self.scroll = QScrollArea()
|
||||
self.scroll.setWidgetResizable(True)
|
||||
self.scroll_content = QWidget()
|
||||
self.grid_layout = QGridLayout(self.scroll_content)
|
||||
self.scroll.setWidget(self.scroll_content)
|
||||
self.layout.addWidget(self.scroll)
|
||||
|
||||
self.thumb_size = QSize(280, 180)
|
||||
self.showMaximized()
|
||||
|
||||
def _create_multiselect_dropdown(self, items):
|
||||
combo = FullClickComboBox()
|
||||
combo.setView(QListView())
|
||||
model = QStandardItemModel()
|
||||
combo.setModel(model)
|
||||
combo.setEditable(True)
|
||||
combo.lineEdit().setReadOnly(True)
|
||||
combo.lineEdit().setPlaceholderText("Select...")
|
||||
|
||||
|
||||
dummy_item = QStandardItem("<None Selected>")
|
||||
dummy_item.setFlags(Qt.ItemIsEnabled)
|
||||
model.appendRow(dummy_item)
|
||||
|
||||
toggle_item = QStandardItem("Toggle Select All")
|
||||
toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
|
||||
toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole)
|
||||
model.appendRow(toggle_item)
|
||||
|
||||
for item in items:
|
||||
standard_item = QStandardItem(item)
|
||||
standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
|
||||
standard_item.setData(Qt.Unchecked, Qt.CheckStateRole)
|
||||
model.appendRow(standard_item)
|
||||
|
||||
combo.setInsertPolicy(QComboBox.NoInsert)
|
||||
|
||||
|
||||
def on_view_clicked(index):
|
||||
item = model.itemFromIndex(index)
|
||||
if item.isCheckable():
|
||||
new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked
|
||||
item.setCheckState(new_state)
|
||||
|
||||
combo.view().pressed.connect(on_view_clicked)
|
||||
|
||||
self._updating_checkstates = False
|
||||
|
||||
def on_item_changed(item):
|
||||
if self._updating_checkstates:
|
||||
return
|
||||
self._updating_checkstates = True
|
||||
|
||||
normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle
|
||||
|
||||
if item == toggle_item:
|
||||
all_checked = all(i.checkState() == Qt.Checked for i in normal_items)
|
||||
if all_checked:
|
||||
for i in normal_items:
|
||||
i.setCheckState(Qt.Unchecked)
|
||||
toggle_item.setCheckState(Qt.Unchecked)
|
||||
else:
|
||||
for i in normal_items:
|
||||
i.setCheckState(Qt.Checked)
|
||||
toggle_item.setCheckState(Qt.Checked)
|
||||
|
||||
elif item == dummy_item:
|
||||
pass
|
||||
|
||||
else:
|
||||
# When normal items change, update toggle item
|
||||
all_checked = all(i.checkState() == Qt.Checked for i in normal_items)
|
||||
toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked)
|
||||
|
||||
# Update label text immediately after change
|
||||
if combo == self.participant_dropdown:
|
||||
self.update_participant_dropdown_label()
|
||||
elif combo == self.image_index_dropdown:
|
||||
self.update_image_index_dropdown_label()
|
||||
|
||||
self._updating_checkstates = False
|
||||
|
||||
model.itemChanged.connect(on_item_changed)
|
||||
|
||||
combo.setInsertPolicy(QComboBox.NoInsert)
|
||||
return combo
|
||||
|
||||
def _get_checked_items(self, combo):
|
||||
checked = []
|
||||
model = combo.model()
|
||||
for i in range(model.rowCount()):
|
||||
item = model.item(i)
|
||||
# Skip dummy and toggle items:
|
||||
if item.text() in ("<None Selected>", "Toggle Select All"):
|
||||
continue
|
||||
if item.checkState() == Qt.Checked:
|
||||
checked.append(item.text())
|
||||
return checked
|
||||
|
||||
def update_participant_dropdown_label(self):
|
||||
selected = self._get_checked_items(self.participant_dropdown)
|
||||
if not selected:
|
||||
self.participant_dropdown.lineEdit().setText("<None Selected>")
|
||||
else:
|
||||
# Extract just "Participant N" from "Participant N (filename)"
|
||||
selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected]
|
||||
self.participant_dropdown.lineEdit().setText(", ".join(selected_short))
|
||||
self._update_event_dropdown()
|
||||
|
||||
def update_image_index_dropdown_label(self):
|
||||
selected = self._get_checked_items(self.image_index_dropdown)
|
||||
if not selected:
|
||||
self.image_index_dropdown.lineEdit().setText("<None Selected>")
|
||||
else:
|
||||
# Only show the index part
|
||||
index_labels = [s.split(" ")[0] for s in selected]
|
||||
self.image_index_dropdown.lineEdit().setText(", ".join(index_labels))
|
||||
|
||||
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
|
||||
|
||||
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 show_brain_images(self):
|
||||
import flares
|
||||
|
||||
selected_event = self.event_dropdown.currentText()
|
||||
if selected_event == "<None Selected>":
|
||||
selected_event = None
|
||||
|
||||
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
|
||||
|
||||
selected_indexes = [
|
||||
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
|
||||
]
|
||||
|
||||
|
||||
parameterized_indexes = {
|
||||
0: [
|
||||
{
|
||||
"key": "n_lines",
|
||||
"label": "<Description>",
|
||||
"default": "20",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"key": "vmin",
|
||||
"label": "<Description>",
|
||||
"default": "0.9",
|
||||
"type": float,
|
||||
},
|
||||
],
|
||||
1: [
|
||||
{
|
||||
"key": "n_lines",
|
||||
"label": "<Description>",
|
||||
"default": "20",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"key": "vmin",
|
||||
"label": "<Description>",
|
||||
"default": "0.9",
|
||||
"type": float,
|
||||
},
|
||||
|
||||
],
|
||||
2: [
|
||||
{
|
||||
"key": "n_lines",
|
||||
"label": "<Description>",
|
||||
"default": "20",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"key": "vmin",
|
||||
"label": "<Description>",
|
||||
"default": "0.9",
|
||||
"type": float,
|
||||
},
|
||||
|
||||
],
|
||||
3: [
|
||||
{
|
||||
"key": "n_lines",
|
||||
"label": "<Description>",
|
||||
"default": "20",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"key": "vmin",
|
||||
"label": "<Description>",
|
||||
"default": "0.9",
|
||||
"type": float,
|
||||
},
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
# Inject full_text from index_texts
|
||||
for idx, params_list in parameterized_indexes.items():
|
||||
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
|
||||
for param_info in params_list:
|
||||
param_info["full_text"] = full_text
|
||||
|
||||
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
|
||||
|
||||
param_values = {}
|
||||
if indexes_needing_params:
|
||||
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
param_values = dialog.get_values()
|
||||
if param_values is None:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
# Pass the necessary arguments to each method
|
||||
for file_path in selected_file_paths:
|
||||
haemo_obj = self.haemo_dict.get(file_path)
|
||||
epochs_obj = self.epochs_dict.get(file_path)
|
||||
|
||||
if haemo_obj is None:
|
||||
raise Exception("How did we get here?")
|
||||
|
||||
|
||||
for idx in selected_indexes:
|
||||
if idx == 0:
|
||||
|
||||
params = param_values.get(idx, {})
|
||||
n_lines = params.get("n_lines", None)
|
||||
vmin = params.get("vmin", None)
|
||||
|
||||
if n_lines is None or vmin is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
flares.functional_connectivity_spectral_epochs(epochs_obj, n_lines, vmin)
|
||||
|
||||
elif idx == 1:
|
||||
params = param_values.get(idx, {})
|
||||
n_lines = params.get("n_lines", None)
|
||||
vmin = params.get("vmin", None)
|
||||
|
||||
if n_lines is None or vmin is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
flares.functional_connectivity_envelope(epochs_obj, n_lines, vmin)
|
||||
|
||||
elif idx == 2:
|
||||
params = param_values.get(idx, {})
|
||||
n_lines = params.get("n_lines", None)
|
||||
vmin = params.get("vmin", None)
|
||||
|
||||
if n_lines is None or vmin is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
flares.functional_connectivity_betas(haemo_obj, n_lines, vmin, selected_event)
|
||||
|
||||
elif idx == 3:
|
||||
params = param_values.get(idx, {})
|
||||
n_lines = params.get("n_lines", None)
|
||||
vmin = params.get("vmin", None)
|
||||
|
||||
if n_lines is None or vmin is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
flares.functional_connectivity_spectral_time(epochs_obj, n_lines, vmin)
|
||||
|
||||
else:
|
||||
print(f"No method defined for index {idx}")
|
||||
|
||||
|
||||
|
||||
class MultiProgressDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -3000,7 +3403,7 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
# Open save dialog
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save SNIRF File As",
|
||||
"Save CSV File As",
|
||||
suggested_name,
|
||||
"CSV Files (*.csv)"
|
||||
)
|
||||
@@ -3017,7 +3420,7 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
QMessageBox.information(self, "Success", "CSV file has been saved.")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
||||
QMessageBox.critical(self, "Error", f"Failed to update CSV file:\n{e}")
|
||||
|
||||
|
||||
elif idx == 1:
|
||||
@@ -3027,7 +3430,7 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
# Open save dialog
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save SNIRF File As",
|
||||
"Save CSV File As",
|
||||
suggested_name,
|
||||
"CSV Files (*.csv)"
|
||||
)
|
||||
@@ -3071,7 +3474,7 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
win.show()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
||||
QMessageBox.critical(self, "Error", f"Failed to update CSV file:\n{e}")
|
||||
|
||||
|
||||
else:
|
||||
@@ -4263,10 +4666,15 @@ class GroupBrainViewerWidget(QWidget):
|
||||
|
||||
|
||||
class ViewerLauncherWidget(QWidget):
|
||||
def __init__(self, haemo_dict, fig_bytes_dict, cha_dict, contrast_results_dict, df_ind, design_matrix, group):
|
||||
def __init__(self, haemo_dict, config_dict, fig_bytes_dict, cha_dict, contrast_results_dict, df_ind, design_matrix, epochs_dict):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Viewer Launcher")
|
||||
|
||||
group_dict = {
|
||||
file_path: config.get("GROUP", "Unknown") # default if GROUP missing
|
||||
for file_path, config in config_dict.items()
|
||||
}
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
btn1 = QPushButton("Open Participant Viewer")
|
||||
@@ -4278,19 +4686,23 @@ class ViewerLauncherWidget(QWidget):
|
||||
btn3 = QPushButton("Open Participant Fold Channels Viewer")
|
||||
btn3.clicked.connect(lambda: self.open_participant_fold_channels_viewer(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))
|
||||
|
||||
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))
|
||||
btn4.clicked.connect(lambda: self.open_group_viewer(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, contrast_results_dict))
|
||||
btn5.clicked.connect(lambda: self.open_group_brain_viewer(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, contrast_results_dict))
|
||||
btn6.clicked.connect(lambda: self.open_export_data_as_csv_viewer(haemo_dict, cha_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
|
||||
|
||||
|
||||
layout.addWidget(btn1)
|
||||
layout.addWidget(btn2)
|
||||
layout.addWidget(btn3)
|
||||
layout.addWidget(btn7)
|
||||
layout.addWidget(btn4)
|
||||
layout.addWidget(btn5)
|
||||
layout.addWidget(btn6)
|
||||
@@ -4307,6 +4719,10 @@ class ViewerLauncherWidget(QWidget):
|
||||
self.participant_fold_channels_viewer = ParticipantFoldChannelsWidget(haemo_dict, cha_dict)
|
||||
self.participant_fold_channels_viewer.show()
|
||||
|
||||
def open_participant_functional_connectivity_viewer(self, haemo_dict, epochs_dict):
|
||||
self.participant_brain_viewer = ParticipantFunctionalConnectivityWidget(haemo_dict, epochs_dict)
|
||||
self.participant_brain_viewer.show()
|
||||
|
||||
def open_group_viewer(self, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group):
|
||||
self.participant_brain_viewer = GroupViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group)
|
||||
self.participant_brain_viewer.show()
|
||||
@@ -4343,7 +4759,8 @@ class MainApplication(QMainWindow):
|
||||
self.section_widget = None
|
||||
self.first_run = True
|
||||
self.is_2d_bypass = False
|
||||
|
||||
self.incompatible_save_bypass = False
|
||||
|
||||
self.files_total = 0 # total number of files to process
|
||||
self.files_done = set() # set of file paths done (success or fail)
|
||||
self.files_failed = set() # set of failed file paths
|
||||
@@ -4593,7 +5010,8 @@ class MainApplication(QMainWindow):
|
||||
|
||||
preferences_menu = menu_bar.addMenu("Preferences")
|
||||
preferences_actions = [
|
||||
("2D Data Bypass", "Ctrl+B", self.is_2d_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg"))
|
||||
("2D Data Bypass", "", self.is_2d_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg")),
|
||||
("Incompatible Save Bypass", "", self.incompatable_save_bypass_func, resource_path("icons/info_24dp_1F1F1F.svg"))
|
||||
]
|
||||
for name, shortcut, slot, icon in preferences_actions:
|
||||
preferences_menu.addAction(make_action(name, shortcut, slot, icon=icon, checkable=True, checked=False))
|
||||
@@ -4643,15 +5061,13 @@ class MainApplication(QMainWindow):
|
||||
self.statusBar().clearMessage()
|
||||
|
||||
self.raw_haemo_dict = None
|
||||
self.config_dict = None
|
||||
self.epochs_dict = None
|
||||
self.fig_bytes_dict = None
|
||||
self.cha_dict = None
|
||||
self.contrast_results_dict = None
|
||||
self.df_ind_dict = None
|
||||
self.design_matrix_dict = None
|
||||
self.age_dict = None
|
||||
self.gender_dict = None
|
||||
self.group_dict = None
|
||||
self.valid_dict = None
|
||||
|
||||
# Reset any visible UI elements
|
||||
@@ -4662,7 +5078,7 @@ class MainApplication(QMainWindow):
|
||||
|
||||
def open_launcher_window(self):
|
||||
|
||||
self.launcher_window = ViewerLauncherWidget(self.raw_haemo_dict, self.fig_bytes_dict, self.cha_dict, self.contrast_results_dict, self.df_ind_dict, self.design_matrix_dict, self.group_dict)
|
||||
self.launcher_window = ViewerLauncherWidget(self.raw_haemo_dict, self.config_dict, self.fig_bytes_dict, self.cha_dict, self.contrast_results_dict, self.df_ind_dict, self.design_matrix_dict, self.epochs_dict)
|
||||
self.launcher_window.show()
|
||||
|
||||
|
||||
@@ -4681,6 +5097,9 @@ class MainApplication(QMainWindow):
|
||||
def is_2d_bypass_func(self, checked):
|
||||
self.is_2d_bypass = checked
|
||||
|
||||
def incompatable_save_bypass_func(self, checked):
|
||||
self.incompatible_save_bypass = checked
|
||||
|
||||
def about_window(self):
|
||||
if self.about is None or not self.about.isVisible():
|
||||
self.about = AboutWindow(self)
|
||||
@@ -4839,19 +5258,19 @@ class MainApplication(QMainWindow):
|
||||
for bubble in self.bubble_widgets.values()
|
||||
}
|
||||
|
||||
version = CURRENT_VERSION
|
||||
project_data = {
|
||||
"version": version,
|
||||
"file_list": file_list,
|
||||
"progress_states": progress_states,
|
||||
"raw_haemo_dict": self.raw_haemo_dict,
|
||||
"config_dict": self.config_dict,
|
||||
"epochs_dict": self.epochs_dict,
|
||||
"fig_bytes_dict": self.fig_bytes_dict,
|
||||
"cha_dict": self.cha_dict,
|
||||
"contrast_results_dict": self.contrast_results_dict,
|
||||
"df_ind_dict": self.df_ind_dict,
|
||||
"design_matrix_dict": self.design_matrix_dict,
|
||||
"age_dict": self.age_dict,
|
||||
"gender_dict": self.gender_dict,
|
||||
"group_dict": self.group_dict,
|
||||
"valid_dict": self.valid_dict,
|
||||
}
|
||||
|
||||
@@ -4866,10 +5285,24 @@ class MainApplication(QMainWindow):
|
||||
|
||||
project_data = sanitize(project_data)
|
||||
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(project_data, f)
|
||||
|
||||
QMessageBox.information(self, "Success", f"Project saved to:\n{filename}")
|
||||
self.saving_overlay = SavingOverlay(self)
|
||||
self.saving_overlay.resize(self.size()) # Cover the main window
|
||||
self.saving_overlay.show()
|
||||
|
||||
# Start the background save thread
|
||||
self.save_thread = SaveProjectThread(filename, project_data)
|
||||
|
||||
# When finished, close overlay and show success
|
||||
self.save_thread.finished_signal.connect(lambda f: (
|
||||
self.saving_overlay.close(),
|
||||
QMessageBox.information(self, "Success", f"Project saved to:\n{f}")
|
||||
))
|
||||
self.save_thread.error_signal.connect(lambda e: (
|
||||
self.saving_overlay.close(),
|
||||
QMessageBox.critical(self, "Error", f"Failed to save project:\n{e}")
|
||||
))
|
||||
|
||||
self.save_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
if not onCrash:
|
||||
@@ -4888,16 +5321,27 @@ class MainApplication(QMainWindow):
|
||||
with open(filename, "rb") as f:
|
||||
data = pickle.load(f)
|
||||
|
||||
# Check for saves prior to 1.2.0
|
||||
if "version" not in data:
|
||||
print(self.incompatible_save_bypass)
|
||||
if self.incompatible_save_bypass:
|
||||
QMessageBox.warning(self, "Warning - FLARES", f"This project was saved in an earlier version of FLARES (<=1.1.7) and is potentially not compatible with this version. "
|
||||
"You are receiving this warning because you have 'Incompatible Save Bypass' turned on. FLARES will now attempt to load the project. It is strongly "
|
||||
"recommended to recreate the project file.")
|
||||
else:
|
||||
QMessageBox.critical(self, "Error - FLARES", f"This project was saved in an earlier version of FLARES (<=1.1.7) and is potentially not compatible with this version. "
|
||||
"The file can attempt to be loaded if 'Incompatible Save Bypass' is selected in the 'Preferences' menu.")
|
||||
return
|
||||
|
||||
|
||||
self.raw_haemo_dict = data.get("raw_haemo_dict", {})
|
||||
self.config_dict = data.get("config_dict", {})
|
||||
self.epochs_dict = data.get("epochs_dict", {})
|
||||
self.fig_bytes_dict = data.get("fig_bytes_dict", {})
|
||||
self.cha_dict = data.get("cha_dict", {})
|
||||
self.contrast_results_dict = data.get("contrast_results_dict", {})
|
||||
self.df_ind_dict = data.get("df_ind_dict", {})
|
||||
self.design_matrix_dict = data.get("design_matrix_dict", {})
|
||||
self.age_dict = data.get("age_dict", {})
|
||||
self.gender_dict = data.get("gender_dict", {})
|
||||
self.group_dict = data.get("group_dict", {})
|
||||
self.valid_dict = data.get("valid_dict", {})
|
||||
|
||||
project_dir = Path(filename).parent
|
||||
@@ -4913,7 +5357,27 @@ 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"]
|
||||
}
|
||||
|
||||
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])
|
||||
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)
|
||||
|
||||
# Re-enable buttons
|
||||
# self.button1.setVisible(True)
|
||||
self.button3.setVisible(True)
|
||||
@@ -4924,6 +5388,54 @@ class MainApplication(QMainWindow):
|
||||
QMessageBox.critical(self, "Error", f"Failed to load project:\n{e}")
|
||||
|
||||
|
||||
def restore_sections_from_config(self, config):
|
||||
"""
|
||||
Fill all ParamSection widgets with values from a participant's config.
|
||||
"""
|
||||
for section_widget in self.param_sections:
|
||||
widgets_dict = getattr(section_widget, 'widgets', None)
|
||||
if widgets_dict is None:
|
||||
continue
|
||||
|
||||
for name, widget_info in widgets_dict.items():
|
||||
if name not in config:
|
||||
continue
|
||||
|
||||
value = config[name]
|
||||
print(f"Restoring {name} = {value}")
|
||||
|
||||
widget = widget_info["widget"]
|
||||
w_type = widget_info.get("type")
|
||||
|
||||
# QLineEdit (int, float, str)
|
||||
if isinstance(widget, QLineEdit):
|
||||
widget.blockSignals(True)
|
||||
widget.setText(str(value))
|
||||
widget.blockSignals(False)
|
||||
widget.update()
|
||||
|
||||
# QComboBox (bool, list)
|
||||
elif isinstance(widget, QComboBox):
|
||||
widget.blockSignals(True)
|
||||
widget.setCurrentText(str(value))
|
||||
widget.blockSignals(False)
|
||||
widget.update()
|
||||
|
||||
# QSpinBox (range)
|
||||
elif isinstance(widget, QSpinBox):
|
||||
widget.blockSignals(True)
|
||||
try:
|
||||
widget.setValue(int(value))
|
||||
except Exception:
|
||||
pass
|
||||
widget.blockSignals(False)
|
||||
widget.update()
|
||||
|
||||
# After restoring, make sure dependencies are updated
|
||||
if hasattr(section_widget, 'update_dependencies'):
|
||||
section_widget.update_dependencies()
|
||||
|
||||
|
||||
|
||||
def show_files_as_bubbles(self, folder_paths):
|
||||
|
||||
@@ -5366,29 +5878,25 @@ class MainApplication(QMainWindow):
|
||||
# TODO: Is this check needed? Edit: yes very much so
|
||||
if getattr(self, 'raw_haemo_dict', None) is None:
|
||||
self.raw_haemo_dict = {}
|
||||
self.config_dict = {}
|
||||
self.epochs_dict = {}
|
||||
self.fig_bytes_dict = {}
|
||||
self.cha_dict = {}
|
||||
self.contrast_results_dict = {}
|
||||
self.df_ind_dict = {}
|
||||
self.design_matrix_dict = {}
|
||||
self.age_dict = {}
|
||||
self.gender_dict = {}
|
||||
self.group_dict = {}
|
||||
self.valid_dict = {}
|
||||
|
||||
# Combine all results into the dicts
|
||||
for file_path, (raw_haemo, epochs, fig_bytes, cha, contrast_results, df_ind, design_matrix, age, gender, group, valid) in results.items():
|
||||
for file_path, (raw_haemo, config, epochs, fig_bytes, cha, contrast_results, df_ind, design_matrix, valid) in results.items():
|
||||
self.raw_haemo_dict[file_path] = raw_haemo
|
||||
self.config_dict[file_path] = config
|
||||
self.epochs_dict[file_path] = epochs
|
||||
self.fig_bytes_dict[file_path] = fig_bytes
|
||||
self.cha_dict[file_path] = cha
|
||||
self.contrast_results_dict[file_path] = contrast_results
|
||||
self.df_ind_dict[file_path] = df_ind
|
||||
self.design_matrix_dict[file_path] = design_matrix
|
||||
self.age_dict[file_path] = age
|
||||
self.gender_dict[file_path] = gender
|
||||
self.group_dict[file_path] = group
|
||||
self.valid_dict[file_path] = valid
|
||||
|
||||
# self.statusbar.showMessage(f"Processing complete! Time elapsed: {elapsed_time:.2f} seconds")
|
||||
|
||||
Reference in New Issue
Block a user