diff --git a/changelog.md b/changelog.md index 0ae0d08..efbc77d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,16 @@ +# Version 1.1.3 + +- Added back the ability to use the fOLD dataset. Fixes [Issue 23](https://git.research.dezeeuw.ca/tyler/flares/issues/23) +- 5th option has been added under Analysis to get to fOLD channels per participant +- Added an option to cancel the running process. Fixes [Issue 15](https://git.research.dezeeuw.ca/tyler/flares/issues/15) +- Prevented graph images from showing when participants are being processed. Fixes [Issue 24](https://git.research.dezeeuw.ca/tyler/flares/issues/24) +- Allow the option to remove all events of a type from all loaded snirfs. Fixes [Issue 25](https://git.research.dezeeuw.ca/tyler/flares/issues/25) +- Added new icons in the menu bar +- Added a terminal to interact with the app in a more command-like form +- Currently the terminal has no functionality but some features for batch operations will be coming soon! +- Inter-Group viewer now has the option to visualize the average response on the brain of all participants in the group. Fixes [Issue 26](https://git.research.dezeeuw.ca/tyler/flares/issues/24) + + # Version 1.1.2 - Fixed incorrect colormaps being applied diff --git a/flares.py b/flares.py index 94c8cf9..586cc01 100644 --- a/flares.py +++ b/flares.py @@ -1077,7 +1077,7 @@ def epochs_calculations(raw_haemo, events, event_dict): # Plot drop log # TODO: Why show this if we never use epochs2? - fig_epochs_dropped = epochs2.plot_drop_log() + fig_epochs_dropped = epochs2.plot_drop_log(show=False) fig_epochs.append(("fig_epochs_dropped", fig_epochs_dropped)) # Plot for each condition diff --git a/icons/terminal_24dp_1F1F1F.svg b/icons/terminal_24dp_1F1F1F.svg new file mode 100644 index 0000000..0a8e6a6 --- /dev/null +++ b/icons/terminal_24dp_1F1F1F.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/upgrade_24dp_1F1F1F.svg b/icons/upgrade_24dp_1F1F1F.svg new file mode 100644 index 0000000..3640fe1 --- /dev/null +++ b/icons/upgrade_24dp_1F1F1F.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/main.py b/main.py index 8a07e77..2714f5d 100644 --- a/main.py +++ b/main.py @@ -42,6 +42,7 @@ from PySide6.QtWidgets import ( ) from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator, QPixmap, QStandardItemModel, QStandardItem +from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen CURRENT_VERSION = "1.0.1" @@ -173,10 +174,10 @@ SECTIONS = [ -class CommandConsole(QWidget): +class TerminalWindow(QWidget): def __init__(self, parent=None): super().__init__(parent, Qt.WindowType.Window) - self.setWindowTitle("Custom Console") + self.setWindowTitle("Terminal - FLARES") self.output_area = QTextEdit() self.output_area.setReadOnly(True) @@ -189,10 +190,8 @@ class CommandConsole(QWidget): 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 } @@ -219,12 +218,9 @@ class CommandConsole(QWidget): 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_hello(self, *args): + return "Hello from the terminal!" def cmd_help(self, *args): return f"Available commands: {', '.join(self.commands.keys())}" @@ -734,13 +730,12 @@ class UpdateEventsWindow(QWidget): self.description = QLabel() self.description.setTextFormat(Qt.TextFormat.RichText) self.description.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) - self.description.setOpenExternalLinks(False) # Handle the click internally + self.description.setOpenExternalLinks(True) - self.description.setText("Some software when creating snirf files will insert a template of optode positions as the correct position of the optodes for the participant.
" - "This is rarely correct as each head differs slightly in shape or size, and a lot of calculations require the optodes to be in the correct location.
" - "Using a .txt file, we can update the positions in the snirf file to match those of a digitization system such as one from Polhemus or elsewhere.
" - "The .txt file should have the fiducials, detectors, and sources clearly labeled, followed by the x, y, and z coordinates seperated by a space.
" - "An example format of what a digitization text file should look like can be found by clicking here.") + self.description.setText("The events that are present in a snirf file may not be the events that are to be studied and examined.
" + "Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.
" + "The software BORIS is used to add these events to video files, and these events can be applied to the snirf file
" + "selected below by selecting the correct BORIS observation and time syncing it to an event that it shares with the snirf file.") layout.addWidget(self.description) @@ -2196,12 +2191,13 @@ class ParticipantFoldChannelsWidget(QWidget): 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.submit_button.clicked.connect(self.show_fold_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(QLabel("This will cause the app to hang for ~30s!")) self.top_bar.addWidget(self.submit_button) self.scroll = QScrollArea() @@ -2321,7 +2317,7 @@ class ParticipantFoldChannelsWidget(QWidget): index_labels = [s.split(" ")[0] for s in selected] self.image_index_dropdown.lineEdit().setText(", ".join(index_labels)) - def show_brain_images(self): + def show_fold_images(self): import flares selected_display_names = self._get_checked_items(self.participant_dropdown) @@ -2337,42 +2333,6 @@ class ParticipantFoldChannelsWidget(QWidget): 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) @@ -2385,14 +2345,6 @@ class ParticipantFoldChannelsWidget(QWidget): 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: @@ -2605,7 +2557,7 @@ class GroupViewerWidget(QWidget): self.index_texts = [ "0 (GLM Results)", "1 (Significance)", - # "2 (third_image)", + "2 (Brain Activity Visualization)", # "3 (fourth image)", ] @@ -2908,6 +2860,32 @@ class GroupViewerWidget(QWidget): "type": float, } ], + 2: [ + { + "key": "show_optodes", + "label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.", + "default": "all", + "type": str, + }, + { + "key": "t_or_theta", + "label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'", + "default": "theta", + "type": str, + }, + { + "key": "show_text", + "label": "Display informative text on the top left corner. THIS DOES NOT WORK AND SHOULD BE LEFT AT FALSE", + "default": "False", + "type": bool, + }, + { + "key": "brain_bounds", + "label": "Graph Upper/Lower Limit", + "default": "1.0", + "type": float, + } + ], } # Inject full_text from index_texts @@ -2930,8 +2908,13 @@ class GroupViewerWidget(QWidget): all_cha = pd.DataFrame() - for fp in selected_file_paths: - cha_df = self.cha.get(fp) + for file_path in selected_file_paths: + haemo_obj = self.haemo_dict.get(file_path) + + if haemo_obj is None: + continue + + cha_df = self.cha.get(file_path) if cha_df is not None: all_cha = pd.concat([all_cha, cha_df], ignore_index=True) @@ -2986,6 +2969,20 @@ class GroupViewerWidget(QWidget): df_contrasts = pd.concat(all_contrasts, ignore_index=True) flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds) + elif idx == 2: + params = param_values.get(idx, {}) + show_optodes = params.get("show_optodes", None) + t_or_theta = params.get("t_or_theta", None) + show_text = params.get("show_text", None) + brain_bounds = params.get("brain_bounds", None) + + if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None: + print(f"Missing parameters for index {idx}, skipping.") + continue + + flares.brain_3d_visualization(haemo_obj, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds) + + elif idx == 3: pass @@ -3840,8 +3837,8 @@ class MainApplication(QMainWindow): options_actions = [ ("User Guide", "F1", self.user_guide, resource_path("icons/help_24dp_1F1F1F.svg")), ("Check for Updates", "F5", self.manual_check_for_updates, resource_path("icons/update_24dp_1F1F1F.svg")), - ("Update optodes in snirf file...", "F6", self.update_optode_positions, resource_path("icons/update_24dp_1F1F1F.svg")), - ("Update events in snirf file...", "F7", self.update_event_markers, resource_path("icons/update_24dp_1F1F1F.svg")), + ("Update optodes in snirf file...", "F6", self.update_optode_positions, resource_path("icons/upgrade_24dp_1F1F1F.svg")), + ("Update events in snirf file...", "F7", self.update_event_markers, resource_path("icons/upgrade_24dp_1F1F1F.svg")), ("About", "F12", self.about_window, resource_path("icons/info_24dp_1F1F1F.svg")) ] @@ -3852,9 +3849,7 @@ class MainApplication(QMainWindow): 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")) + ("New Terminal", "Ctrl+Alt+T", self.terminal_gui, resource_path("icons/terminal_24dp_1F1F1F.svg")), ] for name, shortcut, slot, icon in terminal_actions: terminal_menu.addAction(make_action(name, shortcut, slot, icon=icon)) @@ -3945,7 +3940,7 @@ class MainApplication(QMainWindow): def terminal_gui(self): if self.terminal is None or not self.terminal.isVisible(): - self.terminal = CommandConsole(self) + self.terminal = TerminalWindow(self) self.terminal.show() def update_optode_positions(self): @@ -4865,7 +4860,7 @@ def resource_path(relative_path): # PyInstaller bundle path base_path = sys._MEIPASS else: - base_path = os.path.abspath(".") + base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path, relative_path)