|
|
|
@@ -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.<br>"
|
|
|
|
|
"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.<br>"
|
|
|
|
|
"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.<br>"
|
|
|
|
|
"The .txt file should have the fiducials, detectors, and sources clearly labeled, followed by the x, y, and z coordinates seperated by a space.<br>"
|
|
|
|
|
"An example format of what a digitization text file should look like can be found <a href='custom_link'>by clicking here</a>.")
|
|
|
|
|
self.description.setText("The events that are present in a snirf file may not be the events that are to be studied and examined.<br>"
|
|
|
|
|
"Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.<br>"
|
|
|
|
|
"The software <a href='https://www.boris.unito.it/'>BORIS</a> is used to add these events to video files, and these events can be applied to the snirf file <br>"
|
|
|
|
|
"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)
|
|
|
|
|
|
|
|
|
@@ -1178,6 +1173,11 @@ class ProgressBubble(QWidget):
|
|
|
|
|
rect.setStyleSheet("background-color: yellow; border: 1px solid gray;")
|
|
|
|
|
else:
|
|
|
|
|
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
|
|
|
|
|
|
|
|
|
def mark_cancelled(self):
|
|
|
|
|
if 0 <= self.current_step < len(self.rects):
|
|
|
|
|
rect = self.rects[self.current_step]
|
|
|
|
|
rect.setStyleSheet("background-color: red; border: 1px solid gray;")
|
|
|
|
|
|
|
|
|
|
def mousePressEvent(self, event):
|
|
|
|
|
self.clicked.emit(self)
|
|
|
|
@@ -2191,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()
|
|
|
|
@@ -2316,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)
|
|
|
|
@@ -2332,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)
|
|
|
|
@@ -2380,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:
|
|
|
|
@@ -2600,7 +2557,7 @@ class GroupViewerWidget(QWidget):
|
|
|
|
|
self.index_texts = [
|
|
|
|
|
"0 (GLM Results)",
|
|
|
|
|
"1 (Significance)",
|
|
|
|
|
# "2 (third_image)",
|
|
|
|
|
"2 (Brain Activity Visualization)",
|
|
|
|
|
# "3 (fourth image)",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
@@ -2903,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
|
|
|
|
@@ -2925,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)
|
|
|
|
|
|
|
|
|
@@ -2981,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
|
|
|
|
|
|
|
|
|
@@ -3606,6 +3608,7 @@ class MainApplication(QMainWindow):
|
|
|
|
|
self.param_sections = []
|
|
|
|
|
self.folder_paths = []
|
|
|
|
|
self.section_widget = None
|
|
|
|
|
self.first_run = True
|
|
|
|
|
|
|
|
|
|
self.init_ui()
|
|
|
|
|
self.create_menu_bar()
|
|
|
|
@@ -3834,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"))
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
@@ -3846,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))
|
|
|
|
@@ -3875,6 +3876,8 @@ class MainApplication(QMainWindow):
|
|
|
|
|
|
|
|
|
|
def clear_all(self):
|
|
|
|
|
|
|
|
|
|
self.cancel_task()
|
|
|
|
|
|
|
|
|
|
self.right_column_widget.hide()
|
|
|
|
|
|
|
|
|
|
# Clear the bubble layout
|
|
|
|
@@ -3937,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):
|
|
|
|
@@ -4284,8 +4287,52 @@ class MainApplication(QMainWindow):
|
|
|
|
|
return self.file_metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cancel_task(self):
|
|
|
|
|
self.button1.clicked.disconnect(self.cancel_task)
|
|
|
|
|
self.button1.setText("Stopping...")
|
|
|
|
|
|
|
|
|
|
if hasattr(self, "result_process") and self.result_process.is_alive():
|
|
|
|
|
parent = psutil.Process(self.result_process.pid)
|
|
|
|
|
children = parent.children(recursive=True)
|
|
|
|
|
for child in children:
|
|
|
|
|
try:
|
|
|
|
|
child.kill()
|
|
|
|
|
except psutil.NoSuchProcess:
|
|
|
|
|
pass
|
|
|
|
|
self.result_process.terminate()
|
|
|
|
|
self.result_process.join()
|
|
|
|
|
|
|
|
|
|
if hasattr(self, "result_timer") and self.result_timer.isActive():
|
|
|
|
|
self.result_timer.stop()
|
|
|
|
|
|
|
|
|
|
# if hasattr(self, "result_process") and self.result_process.is_alive():
|
|
|
|
|
# self.result_process.terminate() # Forcefully terminate the process
|
|
|
|
|
# self.result_process.join() # Wait for it to properly close
|
|
|
|
|
|
|
|
|
|
# # Stop the QTimer if running
|
|
|
|
|
# if hasattr(self, "result_timer") and self.result_timer.isActive():
|
|
|
|
|
# self.result_timer.stop()
|
|
|
|
|
|
|
|
|
|
for bubble in self.bubble_widgets.values():
|
|
|
|
|
bubble.mark_cancelled()
|
|
|
|
|
|
|
|
|
|
self.statusbar.showMessage("Processing cancelled.")
|
|
|
|
|
self.button1.clicked.connect(self.on_run_task)
|
|
|
|
|
self.button1.setText("Process")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'''MODULE FILE'''
|
|
|
|
|
def on_run_task(self):
|
|
|
|
|
|
|
|
|
|
self.button1.clicked.disconnect(self.on_run_task)
|
|
|
|
|
self.button1.setText("Cancel")
|
|
|
|
|
self.button1.clicked.connect(self.cancel_task)
|
|
|
|
|
|
|
|
|
|
if not self.first_run:
|
|
|
|
|
for bubble in self.bubble_widgets.values():
|
|
|
|
|
bubble.mark_cancelled()
|
|
|
|
|
self.first_run = False
|
|
|
|
|
|
|
|
|
|
# Collect all selected snirf files in a flat list
|
|
|
|
|
snirf_files = []
|
|
|
|
|
|
|
|
|
@@ -4327,7 +4374,7 @@ class MainApplication(QMainWindow):
|
|
|
|
|
self.manager = Manager()
|
|
|
|
|
self.result_queue = self.manager.Queue()
|
|
|
|
|
self.progress_queue = self.manager.Queue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.result_process = Process(
|
|
|
|
|
target=run_gui_entry_wrapper,
|
|
|
|
|
args=(collected_data, self.result_queue, self.progress_queue)
|
|
|
|
@@ -4813,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)
|
|
|
|
|
|
|
|
|
|