From 87073fb218fcd17fad3aef62be8d99c1cd1539fb Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 15 Oct 2025 10:00:44 -0700 Subject: [PATCH] more boris implementation --- flares.py | 33 ++- main.py | 606 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 611 insertions(+), 28 deletions(-) diff --git a/flares.py b/flares.py index 881a755..94c8cf9 100644 --- a/flares.py +++ b/flares.py @@ -55,7 +55,7 @@ import pyvistaqt # type: ignore # External library imports for mne from mne import ( - EvokedArray, SourceEstimate, Info, Epochs, Label, + EvokedArray, SourceEstimate, Info, Epochs, Label, Annotations, events_from_annotations, read_source_spaces, stc_near_sensors, pick_types, grand_average, get_config, set_config, read_labels_from_annot ) # type: ignore @@ -132,6 +132,8 @@ ENHANCE_NEGATIVE_CORRELATION: bool SHORT_CHANNEL: bool +REMOVE_EVENTS: list + VERBOSITY = True # FIXME: Shouldn't need each ordering - just order it before checking @@ -179,6 +181,7 @@ REQUIRED_KEYS: dict[str, Any] = { "PSP_THRESHOLD": float, "SHORT_CHANNEL": bool, + "REMOVE_EVENTS": list, # "REJECT_PAIRS": bool, # "FORCE_DROP_ANNOTATIONS": list, # "FILTER_LOW_PASS": float, @@ -1470,9 +1473,15 @@ def resource_path(relative_path): def fold_channels(raw: BaseRaw) -> None: + # if getattr(sys, 'frozen', False): + path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary" + logger.info(path) + set_config('MNE_NIRS_FOLD_PATH', resource_path(path)) # type: ignore - # Locate the fOLD excel files - set_config('MNE_NIRS_FOLD_PATH', resource_path("../../mne_data/fOLD/fOLD-public-master/Supplementary")) # type: ignore + # # Locate the fOLD excel files + # else: + # logger.info("yabba") + # set_config('MNE_NIRS_FOLD_PATH', resource_path("../../mne_data/fOLD/fOLD-public-master/Supplementary")) # type: ignore output = None @@ -1534,8 +1543,8 @@ def fold_channels(raw: BaseRaw) -> None: "Brain_Outside", ] - cmap1 = plt.cm.get_cmap('tab20') # First 20 colors - cmap2 = plt.cm.get_cmap('tab20b') # Next 20 colors + cmap1 = plt.get_cmap('tab20') # First 20 colors + cmap2 = plt.get_cmap('tab20b') # Next 20 colors # Combine the colors from both colormaps colors = [cmap1(i) for i in range(20)] + [cmap2(i) for i in range(20)] # Total 40 colors @@ -1611,6 +1620,7 @@ def fold_channels(raw: BaseRaw) -> None: for ax in axes[len(hbo_channel_names):]: ax.axis('off') + plt.show() return fig, legend_fig @@ -2935,6 +2945,19 @@ def process_participant(file_path, progress_callback=None): logger.info("14") # Step 14: Design Matrix + events_to_remove = REMOVE_EVENTS + + filtered_annotations = [ann for ann in raw.annotations if ann['description'] not in events_to_remove] + + new_annot = Annotations( + onset=[ann['onset'] for ann in filtered_annotations], + duration=[ann['duration'] for ann in filtered_annotations], + description=[ann['description'] for ann in filtered_annotations] + ) + + # Set the new annotations + raw_haemo.set_annotations(new_annot) + design_matrix, fig_design_matrix = make_design_matrix(raw_haemo, short_chans) fig_individual["Design Matrix"] = fig_design_matrix if progress_callback: progress_callback(15) diff --git a/main.py b/main.py index a44b1b8..05432e2 100644 --- a/main.py +++ b/main.py @@ -47,6 +47,8 @@ from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleVa CURRENT_VERSION = "1.0.1" API_URL = "https://git.research.dezeeuw.ca/api/v1/repos/tyler/flares/releases" +API_URL_SECONDARY = "https://git.research2.dezeeuw.ca/api/v1/repos/tyler/flares/releases" + PLATFORM_NAME = platform.system().lower() # Selectable parameters on the right side of the window @@ -143,6 +145,7 @@ SECTIONS = [ { "title": "Design Matrix", "params": [ + {"name": "REMOVE_EVENTS", "default": "None", "type": list, "help": "Remove events matching the names provided before generating the Design Matrix"}, # {"name": "DRIFT_MODEL", "default": "cosine", "type": str, "help": "Drift model for GLM."}, # {"name": "DURATION_BETWEEN_ACTIVITIES", "default": 35, "type": int, "help": "Time between activities (s)."}, # {"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "help": "Use short channel regression."}, @@ -170,6 +173,63 @@ SECTIONS = [ +class CommandConsole(QWidget): + def __init__(self, parent=None): + super().__init__(parent, Qt.WindowType.Window) + self.setWindowTitle("Custom Console") + + self.output_area = QTextEdit() + self.output_area.setReadOnly(True) + + self.input_line = QLineEdit() + self.input_line.returnPressed.connect(self.handle_command) + + layout = QVBoxLayout() + layout.addWidget(self.output_area) + layout.addWidget(self.input_line) + self.setLayout(layout) + + # Define your commands + self.commands = { + "hello": self.cmd_hello, + "add": self.cmd_add, + "help": self.cmd_help + } + + def handle_command(self): + command_text = self.input_line.text() + self.input_line.clear() + + self.output_area.append(f"> {command_text}") + parts = command_text.strip().split() + if not parts: + return + + command_name = parts[0] + args = parts[1:] + + func = self.commands.get(command_name) + if func: + try: + result = func(*args) + if result: + self.output_area.append(str(result)) + except Exception as e: + self.output_area.append(f"[Error] {e}") + else: + self.output_area.append(f"[Unknown command] '{command_name}'") + + # Example commands + def cmd_hello(self, *args): + return "Hello from the console!" + + def cmd_add(self, a, b): + return f"Result: {int(a) + int(b)}" + + def cmd_help(self, *args): + return f"Available commands: {', '.join(self.commands.keys())}" + + class UpdateDownloadThread(QThread): """ Thread that downloads and extracts an update package and emits a signal on completion or error. @@ -276,22 +336,28 @@ class UpdateCheckThread(QThread): return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2)) def get_latest_release_for_platform(self): - response = requests.get(API_URL, timeout=5) - response.raise_for_status() - releases = response.json() + urls = [API_URL, API_URL_SECONDARY] + for url in urls: + try: + + response = requests.get(API_URL, timeout=5) + response.raise_for_status() + releases = response.json() - if not releases: - return None, None + if not releases: + return None, None - latest = releases[0] - tag = latest["tag_name"].lstrip("v") + latest = releases[0] + tag = latest["tag_name"].lstrip("v") - for asset in latest.get("assets", []): - if PLATFORM_NAME in asset["name"].lower(): - return tag, asset["browser_download_url"] + for asset in latest.get("assets", []): + if PLATFORM_NAME in asset["name"].lower(): + return tag, asset["browser_download_url"] - return tag, None - + return tag, None + except (requests.RequestException, ValueError) as e: + continue + return None, None class LocalPendingUpdateCheckThread(QThread): @@ -1201,6 +1267,8 @@ class ParamSection(QWidget): widget = QLineEdit() widget.setValidator(QDoubleValidator()) widget.setText(str(param["default"])) + elif param["type"] == list: + widget = self._create_multiselect_dropdown(None) else: widget = QLineEdit() widget.setText(str(param["default"])) @@ -1216,6 +1284,81 @@ class ParamSection(QWidget): "type": param["type"] } + 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("") + 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) + + if items is not None: + 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) + + self._updating_checkstates = False + + for param_name, info in self.widgets.items(): + if info["widget"] == combo: + self.update_dropdown_label(param_name) + break + + model.itemChanged.connect(on_item_changed) + + combo.setInsertPolicy(QComboBox.NoInsert) + return combo def show_help_popup(self, text): msg = QMessageBox(self) @@ -1232,6 +1375,8 @@ class ParamSection(QWidget): if expected_type == bool: values[name] = widget.currentText() == "True" + elif expected_type == list: + values[name] = [x.strip() for x in widget.lineEdit().text().split(",") if x.strip()] else: raw_text = widget.text() try: @@ -1241,9 +1386,6 @@ class ParamSection(QWidget): values[name] = float(raw_text) elif expected_type == str: values[name] = raw_text - elif expected_type == list: - # Very basic CSV parsing - fix? - values[name] = [x.strip() for x in raw_text.split(",") if x.strip()] else: values[name] = raw_text # Fallback except Exception as e: @@ -1251,6 +1393,155 @@ class ParamSection(QWidget): return values + def update_dropdown_items(self, param_name, new_items): + """ + Updates the items in a multi-select dropdown parameter field. + + Args: + param_name (str): The parameter name (must match one in self.widgets). + new_items (list): The new items to populate in the dropdown. + """ + widget_info = self.widgets.get(param_name) + print("[ParamSection] Current widget keys:", list(self.widgets.keys())) + + if not widget_info: + print(f"[ParamSection] No widget found for param '{param_name}'") + return + + widget = widget_info["widget"] + if not isinstance(widget, FullClickComboBox): + print(f"[ParamSection] Widget for param '{param_name}' is not a FullClickComboBox") + return + + # Replace the model on the existing widget + new_model = QStandardItemModel() + + dummy_item = QStandardItem("") + dummy_item.setFlags(Qt.ItemIsEnabled) + new_model.appendRow(dummy_item) + + toggle_item = QStandardItem("Toggle Select All") + toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole) + new_model.appendRow(toggle_item) + + for item_text in new_items: + item = QStandardItem(item_text) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + new_model.appendRow(item) + + widget.setModel(new_model) + widget.setView(QListView()) # Reset view to refresh properly + + def on_view_clicked(index): + item = new_model.itemFromIndex(index) + if item.isCheckable(): + new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked + item.setCheckState(new_state) + + widget.view().pressed.connect(on_view_clicked) + + def on_item_changed(item): + if getattr(self, "_updating_checkstates", False): + return + self._updating_checkstates = True + + normal_items = [new_model.item(i) for i in range(2, new_model.rowCount())] + if item == toggle_item: + all_checked = all(i.checkState() == Qt.Checked for i in normal_items) + for i in normal_items: + i.setCheckState(Qt.Unchecked if all_checked else Qt.Checked) + toggle_item.setCheckState(Qt.Unchecked if all_checked else Qt.Checked) + else: + all_checked = all(i.checkState() == Qt.Checked for i in normal_items) + toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked) + + self._updating_checkstates = False + + for param_name, info in self.widgets.items(): + if info["widget"] == widget: + self.update_dropdown_label(param_name) + break + + new_model.itemChanged.connect(on_item_changed) + widget.lineEdit().setText("") + + + def _get_checked_items(self, combo): + checked = [] + model = combo.model() + for i in range(model.rowCount()): + item = model.item(i) + if item.text() in ("", "Toggle Select All"): + continue + if item.checkState() == Qt.Checked: + checked.append(item.text()) + return checked + + def update_dropdown_label(self, param_name): + widget_info = self.widgets.get(param_name) + if not widget_info: + print(f"[ParamSection] No widget found for param '{param_name}'") + return + + widget = widget_info["widget"] + if not isinstance(widget, FullClickComboBox): + print(f"[ParamSection] Widget for param '{param_name}' is not a FullClickComboBox") + return + + selected = self._get_checked_items(widget) + if not selected: + widget.lineEdit().setText("") + else: + # You can customize how you display selected items here: + widget.lineEdit().setText(", ".join(selected)) + + def update_annotation_dropdown_from_folder(self, folder_path): + """ + Reads all EEG files in the given folder, extracts annotations using MNE, + and updates the dropdown for the `target_param` with the set of common annotations. + + Args: + folder_param (str): The name of the parameter holding the folder path. + target_param (str): The name of the multi-select dropdown to update. + """ + # folder_path_widget = self.widgets.get(folder_param, {}).get("widget") + # if not folder_path_widget: + # print(f"[ParamSection] Folder path param '{folder_param}' not found.") + # return + + # folder_path = folder_path_widget.text().strip() + # if not os.path.isdir(folder_path): + # print(f"[ParamSection] '{folder_path}' is not a valid directory.") + # return + + annotation_sets = [] + for filename in os.listdir(folder_path): + full_path = os.path.join(folder_path, filename) + try: + raw = read_raw_snirf(full_path, preload=False, verbose="ERROR") + annotations = raw.annotations + if annotations is not None: + labels = set(annotations.description) + annotation_sets.append(labels) + except Exception as e: + print(f"[ParamSection] Skipping file '{filename}' due to error: {e}") + + if not annotation_sets: + print(f"[ParamSection] No annotations found in folder '{folder_path}'") + return + + # Get common annotations + common_annotations = set.intersection(*annotation_sets) if len(annotation_sets) > 1 else annotation_sets[0] + common_annotations = sorted(list(common_annotations)) # for consistent order + + print(f"[ParamSection] Common annotations: {common_annotations}") + + # Update the dropdown + + self.update_dropdown_items("REMOVE_EVENTS", common_annotations) + class FullClickLineEdit(QLineEdit): def mousePressEvent(self, event): @@ -1865,6 +2156,245 @@ class ParticipantBrainViewerWidget(QWidget): +class ParticipantFoldChannelsWidget(QWidget): + def __init__(self, haemo_dict, cha_dict): + super().__init__() + self.setWindowTitle("FLARES Participant Fold Channels Viewer") + self.haemo_dict = haemo_dict + self.cha_dict = cha_dict + + # 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.index_texts = [ + "0 (Fold Channels)", + # "1 (second image)", + # "2 (third image)", + # "3 (fourth image)", + ] + + 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("Fold Type:")) + 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("") + 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 ("", "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("") + 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)) + + 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("") + 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 show_brain_images(self): + import flares + + 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": "show_optodes", + "label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.", + "default": "all", + "type": str, + }, + { + "key": "show_brodmann", + "label": "Show common brodmann areas on the brain.", + "default": "True", + "type": bool, + } + ], + } + + # 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) + + if haemo_obj is None: + continue + + #cha = self.cha_dict.get(file_path) + + for idx in selected_indexes: + if idx == 0: + + params = param_values.get(idx, {}) + show_optodes = params.get("show_optodes", None) + show_brodmann = params.get("show_brodmann", None) + + if show_optodes is None or show_brodmann is None: + print(f"Missing parameters for index {idx}, skipping.") + continue + + flares.fold_channels(haemo_obj) + + else: + print(f"No method defined for index {idx}") + + + class ClickableLabel(QLabel): def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap): super().__init__() @@ -3017,18 +3547,22 @@ class ViewerLauncherWidget(QWidget): btn2 = QPushButton("Open Participant Brain Viewer") btn2.clicked.connect(lambda: self.open_participant_brain_viewer(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 = QPushButton("Open Inter-Group Viewer") - btn3.clicked.connect(lambda: self.open_group_viewer(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group)) - - btn4 = QPushButton("Open Cross Group Brain Viewer") - btn4.clicked.connect(lambda: self.open_group_brain_viewer(haemo_dict, df_ind, design_matrix, group, contrast_results_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)) + + 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)) layout.addWidget(btn1) layout.addWidget(btn2) layout.addWidget(btn3) layout.addWidget(btn4) + layout.addWidget(btn5) def open_participant_viewer(self, haemo_dict, fig_bytes_dict): self.participant_viewer = ParticipantViewerWidget(haemo_dict, fig_bytes_dict) @@ -3037,6 +3571,10 @@ class ViewerLauncherWidget(QWidget): def open_participant_brain_viewer(self, haemo_dict, cha_dict): self.participant_brain_viewer = ParticipantBrainViewerWidget(haemo_dict, cha_dict) self.participant_brain_viewer.show() + + def open_participant_fold_channels_viewer(self, haemo_dict, cha_dict): + self.participant_fold_channels_viewer = ParticipantFoldChannelsWidget(haemo_dict, cha_dict) + self.participant_fold_channels_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) @@ -3063,9 +3601,11 @@ class MainApplication(QMainWindow): self.help = None self.optodes = None self.events = None + self.terminal = None self.bubble_widgets = {} self.param_sections = [] self.folder_paths = [] + self.section_widget = None self.init_ui() self.create_menu_bar() @@ -3304,6 +3844,15 @@ class MainApplication(QMainWindow): if i == 1 or i == 3: # after the first 2 actions (0,1) options_menu.addSeparator() + terminal_menu = menu_bar.addMenu("Terminal") + terminal_actions = [ + ("Cut", "Ctrl+X", self.terminal_gui, resource_path("icons/content_cut_24dp_1F1F1F.svg")), + ("Copy", "Ctrl+C", self.terminal_gui, resource_path("icons/content_copy_24dp_1F1F1F.svg")), + ("Paste", "Ctrl+V", self.terminal_gui, resource_path("icons/content_paste_24dp_1F1F1F.svg")) + ] + for name, shortcut, slot, icon in terminal_actions: + terminal_menu.addAction(make_action(name, shortcut, slot, icon=icon)) + self.statusbar = self.statusBar() self.statusbar.showMessage("Ready") @@ -3318,10 +3867,10 @@ class MainApplication(QMainWindow): # Add ParamSection widgets from SECTIONS for section in SECTIONS: - section_widget = ParamSection(section) - self.rows_layout.addWidget(section_widget) + self.section_widget = ParamSection(section) + self.rows_layout.addWidget(self.section_widget) - self.param_sections.append(section_widget) + self.param_sections.append(self.section_widget) def clear_all(self): @@ -3385,6 +3934,11 @@ class MainApplication(QMainWindow): if self.help is None or not self.help.isVisible(): self.help = UserGuideWindow(self) self.help.show() + + def terminal_gui(self): + if self.terminal is None or not self.terminal.isVisible(): + self.terminal = CommandConsole(self) + self.terminal.show() def update_optode_positions(self): if self.optodes is None or not self.optodes.isVisible(): @@ -3415,6 +3969,12 @@ class MainApplication(QMainWindow): if folder_path: self.selected_path = folder_path # store the folder path self.show_files_as_bubbles(folder_path) + for section_widget in self.param_sections: + if "REMOVE_EVENTS" in section_widget.widgets: + section_widget.update_annotation_dropdown_from_folder(folder_path) + break + else: + print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget") self.button1.setVisible(True)