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)