Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0f70ea49 | |||
| d6c71e0ab2 | |||
| 87073fb218 |
19
changelog.md
19
changelog.md
@@ -1,3 +1,22 @@
|
|||||||
|
# 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
|
||||||
|
- Added functionality to utilize external event markers from a file. Fixes [Issue 6](https://git.research.dezeeuw.ca/tyler/flares/issues/6)
|
||||||
|
|
||||||
|
|
||||||
# Version 1.1.1
|
# Version 1.1.1
|
||||||
|
|
||||||
- Fixed the number of rectangles in the progress bar to 19
|
- Fixed the number of rectangles in the progress bar to 19
|
||||||
|
|||||||
35
flares.py
35
flares.py
@@ -55,7 +55,7 @@ import pyvistaqt # type: ignore
|
|||||||
|
|
||||||
# External library imports for mne
|
# External library imports for mne
|
||||||
from mne import (
|
from mne import (
|
||||||
EvokedArray, SourceEstimate, Info, Epochs, Label,
|
EvokedArray, SourceEstimate, Info, Epochs, Label, Annotations,
|
||||||
events_from_annotations, read_source_spaces,
|
events_from_annotations, read_source_spaces,
|
||||||
stc_near_sensors, pick_types, grand_average, get_config, set_config, read_labels_from_annot
|
stc_near_sensors, pick_types, grand_average, get_config, set_config, read_labels_from_annot
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
@@ -132,6 +132,8 @@ ENHANCE_NEGATIVE_CORRELATION: bool
|
|||||||
|
|
||||||
SHORT_CHANNEL: bool
|
SHORT_CHANNEL: bool
|
||||||
|
|
||||||
|
REMOVE_EVENTS: list
|
||||||
|
|
||||||
VERBOSITY = True
|
VERBOSITY = True
|
||||||
|
|
||||||
# FIXME: Shouldn't need each ordering - just order it before checking
|
# FIXME: Shouldn't need each ordering - just order it before checking
|
||||||
@@ -179,6 +181,7 @@ REQUIRED_KEYS: dict[str, Any] = {
|
|||||||
"PSP_THRESHOLD": float,
|
"PSP_THRESHOLD": float,
|
||||||
|
|
||||||
"SHORT_CHANNEL": bool,
|
"SHORT_CHANNEL": bool,
|
||||||
|
"REMOVE_EVENTS": list,
|
||||||
# "REJECT_PAIRS": bool,
|
# "REJECT_PAIRS": bool,
|
||||||
# "FORCE_DROP_ANNOTATIONS": list,
|
# "FORCE_DROP_ANNOTATIONS": list,
|
||||||
# "FILTER_LOW_PASS": float,
|
# "FILTER_LOW_PASS": float,
|
||||||
@@ -1074,7 +1077,7 @@ def epochs_calculations(raw_haemo, events, event_dict):
|
|||||||
|
|
||||||
# Plot drop log
|
# Plot drop log
|
||||||
# TODO: Why show this if we never use epochs2?
|
# 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))
|
fig_epochs.append(("fig_epochs_dropped", fig_epochs_dropped))
|
||||||
|
|
||||||
# Plot for each condition
|
# Plot for each condition
|
||||||
@@ -1470,9 +1473,15 @@ def resource_path(relative_path):
|
|||||||
|
|
||||||
def fold_channels(raw: BaseRaw) -> None:
|
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
|
# # Locate the fOLD excel files
|
||||||
set_config('MNE_NIRS_FOLD_PATH', resource_path("../../mne_data/fOLD/fOLD-public-master/Supplementary")) # type: ignore
|
# else:
|
||||||
|
# logger.info("yabba")
|
||||||
|
# set_config('MNE_NIRS_FOLD_PATH', resource_path("../../mne_data/fOLD/fOLD-public-master/Supplementary")) # type: ignore
|
||||||
|
|
||||||
output = None
|
output = None
|
||||||
|
|
||||||
@@ -1534,8 +1543,8 @@ def fold_channels(raw: BaseRaw) -> None:
|
|||||||
"Brain_Outside",
|
"Brain_Outside",
|
||||||
]
|
]
|
||||||
|
|
||||||
cmap1 = plt.cm.get_cmap('tab20') # First 20 colors
|
cmap1 = plt.get_cmap('tab20') # First 20 colors
|
||||||
cmap2 = plt.cm.get_cmap('tab20b') # Next 20 colors
|
cmap2 = plt.get_cmap('tab20b') # Next 20 colors
|
||||||
|
|
||||||
# Combine the colors from both colormaps
|
# Combine the colors from both colormaps
|
||||||
colors = [cmap1(i) for i in range(20)] + [cmap2(i) for i in range(20)] # Total 40 colors
|
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):]:
|
for ax in axes[len(hbo_channel_names):]:
|
||||||
ax.axis('off')
|
ax.axis('off')
|
||||||
|
|
||||||
|
plt.show()
|
||||||
return fig, legend_fig
|
return fig, legend_fig
|
||||||
|
|
||||||
|
|
||||||
@@ -2935,6 +2945,19 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
logger.info("14")
|
logger.info("14")
|
||||||
|
|
||||||
# Step 14: Design Matrix
|
# 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)
|
design_matrix, fig_design_matrix = make_design_matrix(raw_haemo, short_chans)
|
||||||
fig_individual["Design Matrix"] = fig_design_matrix
|
fig_individual["Design Matrix"] = fig_design_matrix
|
||||||
if progress_callback: progress_callback(15)
|
if progress_callback: progress_callback(15)
|
||||||
|
|||||||
1
icons/terminal_24dp_1F1F1F.svg
Normal file
1
icons/terminal_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H160v400Zm140-40-56-56 103-104-104-104 57-56 160 160-160 160Zm180 0v-80h240v80H480Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
1
icons/upgrade_24dp_1F1F1F.svg
Normal file
1
icons/upgrade_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-160v-80h400v80H280Zm160-160v-327L336-544l-56-56 200-200 200 200-56 56-104-103v327h-80Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 216 B |
653
main.py
653
main.py
@@ -42,11 +42,14 @@ from PySide6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize
|
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize
|
||||||
from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator, QPixmap, QStandardItemModel, QStandardItem
|
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"
|
CURRENT_VERSION = "1.0.1"
|
||||||
|
|
||||||
API_URL = "https://git.research.dezeeuw.ca/api/v1/repos/tyler/flares/releases"
|
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()
|
PLATFORM_NAME = platform.system().lower()
|
||||||
|
|
||||||
# Selectable parameters on the right side of the window
|
# Selectable parameters on the right side of the window
|
||||||
@@ -143,6 +146,7 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Design Matrix",
|
"title": "Design Matrix",
|
||||||
"params": [
|
"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": "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": "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."},
|
# {"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "help": "Use short channel regression."},
|
||||||
@@ -170,6 +174,58 @@ SECTIONS = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalWindow(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
|
self.setWindowTitle("Terminal - FLARES")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.commands = {
|
||||||
|
"hello": self.cmd_hello,
|
||||||
|
"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}'")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_hello(self, *args):
|
||||||
|
return "Hello from the terminal!"
|
||||||
|
|
||||||
|
def cmd_help(self, *args):
|
||||||
|
return f"Available commands: {', '.join(self.commands.keys())}"
|
||||||
|
|
||||||
|
|
||||||
class UpdateDownloadThread(QThread):
|
class UpdateDownloadThread(QThread):
|
||||||
"""
|
"""
|
||||||
Thread that downloads and extracts an update package and emits a signal on completion or error.
|
Thread that downloads and extracts an update package and emits a signal on completion or error.
|
||||||
@@ -276,6 +332,10 @@ class UpdateCheckThread(QThread):
|
|||||||
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
|
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
|
||||||
|
|
||||||
def get_latest_release_for_platform(self):
|
def get_latest_release_for_platform(self):
|
||||||
|
urls = [API_URL, API_URL_SECONDARY]
|
||||||
|
for url in urls:
|
||||||
|
try:
|
||||||
|
|
||||||
response = requests.get(API_URL, timeout=5)
|
response = requests.get(API_URL, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
releases = response.json()
|
releases = response.json()
|
||||||
@@ -291,7 +351,9 @@ class UpdateCheckThread(QThread):
|
|||||||
return tag, asset["browser_download_url"]
|
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):
|
class LocalPendingUpdateCheckThread(QThread):
|
||||||
@@ -668,13 +730,12 @@ class UpdateEventsWindow(QWidget):
|
|||||||
self.description = QLabel()
|
self.description = QLabel()
|
||||||
self.description.setTextFormat(Qt.TextFormat.RichText)
|
self.description.setTextFormat(Qt.TextFormat.RichText)
|
||||||
self.description.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
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>"
|
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>"
|
||||||
"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>"
|
"Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.<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 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>"
|
||||||
"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>"
|
"selected below by selecting the correct BORIS observation and time syncing it to an event that it shares with the snirf file.")
|
||||||
"An example format of what a digitization text file should look like can be found <a href='custom_link'>by clicking here</a>.")
|
|
||||||
|
|
||||||
layout.addWidget(self.description)
|
layout.addWidget(self.description)
|
||||||
|
|
||||||
@@ -1113,6 +1174,11 @@ class ProgressBubble(QWidget):
|
|||||||
else:
|
else:
|
||||||
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
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):
|
def mousePressEvent(self, event):
|
||||||
self.clicked.emit(self)
|
self.clicked.emit(self)
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
@@ -1201,6 +1267,8 @@ class ParamSection(QWidget):
|
|||||||
widget = QLineEdit()
|
widget = QLineEdit()
|
||||||
widget.setValidator(QDoubleValidator())
|
widget.setValidator(QDoubleValidator())
|
||||||
widget.setText(str(param["default"]))
|
widget.setText(str(param["default"]))
|
||||||
|
elif param["type"] == list:
|
||||||
|
widget = self._create_multiselect_dropdown(None)
|
||||||
else:
|
else:
|
||||||
widget = QLineEdit()
|
widget = QLineEdit()
|
||||||
widget.setText(str(param["default"]))
|
widget.setText(str(param["default"]))
|
||||||
@@ -1216,6 +1284,81 @@ class ParamSection(QWidget):
|
|||||||
"type": param["type"]
|
"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("<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)
|
||||||
|
|
||||||
|
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):
|
def show_help_popup(self, text):
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
@@ -1232,6 +1375,8 @@ class ParamSection(QWidget):
|
|||||||
|
|
||||||
if expected_type == bool:
|
if expected_type == bool:
|
||||||
values[name] = widget.currentText() == "True"
|
values[name] = widget.currentText() == "True"
|
||||||
|
elif expected_type == list:
|
||||||
|
values[name] = [x.strip() for x in widget.lineEdit().text().split(",") if x.strip()]
|
||||||
else:
|
else:
|
||||||
raw_text = widget.text()
|
raw_text = widget.text()
|
||||||
try:
|
try:
|
||||||
@@ -1241,9 +1386,6 @@ class ParamSection(QWidget):
|
|||||||
values[name] = float(raw_text)
|
values[name] = float(raw_text)
|
||||||
elif expected_type == str:
|
elif expected_type == str:
|
||||||
values[name] = raw_text
|
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:
|
else:
|
||||||
values[name] = raw_text # Fallback
|
values[name] = raw_text # Fallback
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1251,6 +1393,155 @@ class ParamSection(QWidget):
|
|||||||
|
|
||||||
return values
|
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("<None Selected>")
|
||||||
|
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("<None Selected>")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_checked_items(self, combo):
|
||||||
|
checked = []
|
||||||
|
model = combo.model()
|
||||||
|
for i in range(model.rowCount()):
|
||||||
|
item = model.item(i)
|
||||||
|
if item.text() in ("<None Selected>", "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("<None Selected>")
|
||||||
|
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):
|
class FullClickLineEdit(QLineEdit):
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
@@ -1865,6 +2156,202 @@ 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_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()
|
||||||
|
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))
|
||||||
|
|
||||||
|
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 show_fold_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)
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
flares.fold_channels(haemo_obj)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"No method defined for index {idx}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ClickableLabel(QLabel):
|
class ClickableLabel(QLabel):
|
||||||
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
|
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -2070,7 +2557,7 @@ class GroupViewerWidget(QWidget):
|
|||||||
self.index_texts = [
|
self.index_texts = [
|
||||||
"0 (GLM Results)",
|
"0 (GLM Results)",
|
||||||
"1 (Significance)",
|
"1 (Significance)",
|
||||||
# "2 (third_image)",
|
"2 (Brain Activity Visualization)",
|
||||||
# "3 (fourth image)",
|
# "3 (fourth image)",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2373,6 +2860,32 @@ class GroupViewerWidget(QWidget):
|
|||||||
"type": float,
|
"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
|
# Inject full_text from index_texts
|
||||||
@@ -2395,8 +2908,13 @@ class GroupViewerWidget(QWidget):
|
|||||||
|
|
||||||
|
|
||||||
all_cha = pd.DataFrame()
|
all_cha = pd.DataFrame()
|
||||||
for fp in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
cha_df = self.cha.get(fp)
|
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:
|
if cha_df is not None:
|
||||||
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
||||||
|
|
||||||
@@ -2451,6 +2969,20 @@ class GroupViewerWidget(QWidget):
|
|||||||
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
|
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
|
||||||
flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
|
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:
|
elif idx == 3:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -3018,17 +3550,21 @@ class ViewerLauncherWidget(QWidget):
|
|||||||
btn2 = QPushButton("Open Participant Brain Viewer")
|
btn2 = QPushButton("Open Participant Brain Viewer")
|
||||||
btn2.clicked.connect(lambda: self.open_participant_brain_viewer(haemo_dict, cha_dict))
|
btn2.clicked.connect(lambda: self.open_participant_brain_viewer(haemo_dict, cha_dict))
|
||||||
|
|
||||||
btn3 = QPushButton("Open Inter-Group Viewer")
|
btn3 = QPushButton("Open Participant Fold Channels Viewer")
|
||||||
btn3.clicked.connect(lambda: self.open_group_viewer(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group))
|
btn3.clicked.connect(lambda: self.open_participant_fold_channels_viewer(haemo_dict, cha_dict))
|
||||||
|
|
||||||
btn4 = QPushButton("Open Cross Group Brain Viewer")
|
btn4 = QPushButton("Open Inter-Group Viewer")
|
||||||
btn4.clicked.connect(lambda: self.open_group_brain_viewer(haemo_dict, df_ind, design_matrix, group, contrast_results_dict))
|
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(btn1)
|
||||||
layout.addWidget(btn2)
|
layout.addWidget(btn2)
|
||||||
layout.addWidget(btn3)
|
layout.addWidget(btn3)
|
||||||
layout.addWidget(btn4)
|
layout.addWidget(btn4)
|
||||||
|
layout.addWidget(btn5)
|
||||||
|
|
||||||
def open_participant_viewer(self, haemo_dict, fig_bytes_dict):
|
def open_participant_viewer(self, haemo_dict, fig_bytes_dict):
|
||||||
self.participant_viewer = ParticipantViewerWidget(haemo_dict, fig_bytes_dict)
|
self.participant_viewer = ParticipantViewerWidget(haemo_dict, fig_bytes_dict)
|
||||||
@@ -3038,6 +3574,10 @@ class ViewerLauncherWidget(QWidget):
|
|||||||
self.participant_brain_viewer = ParticipantBrainViewerWidget(haemo_dict, cha_dict)
|
self.participant_brain_viewer = ParticipantBrainViewerWidget(haemo_dict, cha_dict)
|
||||||
self.participant_brain_viewer.show()
|
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):
|
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 = GroupViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group)
|
||||||
self.participant_brain_viewer.show()
|
self.participant_brain_viewer.show()
|
||||||
@@ -3063,9 +3603,12 @@ class MainApplication(QMainWindow):
|
|||||||
self.help = None
|
self.help = None
|
||||||
self.optodes = None
|
self.optodes = None
|
||||||
self.events = None
|
self.events = None
|
||||||
|
self.terminal = None
|
||||||
self.bubble_widgets = {}
|
self.bubble_widgets = {}
|
||||||
self.param_sections = []
|
self.param_sections = []
|
||||||
self.folder_paths = []
|
self.folder_paths = []
|
||||||
|
self.section_widget = None
|
||||||
|
self.first_run = True
|
||||||
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.create_menu_bar()
|
self.create_menu_bar()
|
||||||
@@ -3294,8 +3837,8 @@ class MainApplication(QMainWindow):
|
|||||||
options_actions = [
|
options_actions = [
|
||||||
("User Guide", "F1", self.user_guide, resource_path("icons/help_24dp_1F1F1F.svg")),
|
("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")),
|
("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 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/update_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"))
|
("About", "F12", self.about_window, resource_path("icons/info_24dp_1F1F1F.svg"))
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3304,6 +3847,13 @@ class MainApplication(QMainWindow):
|
|||||||
if i == 1 or i == 3: # after the first 2 actions (0,1)
|
if i == 1 or i == 3: # after the first 2 actions (0,1)
|
||||||
options_menu.addSeparator()
|
options_menu.addSeparator()
|
||||||
|
|
||||||
|
terminal_menu = menu_bar.addMenu("Terminal")
|
||||||
|
terminal_actions = [
|
||||||
|
("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))
|
||||||
|
|
||||||
self.statusbar = self.statusBar()
|
self.statusbar = self.statusBar()
|
||||||
self.statusbar.showMessage("Ready")
|
self.statusbar.showMessage("Ready")
|
||||||
|
|
||||||
@@ -3318,14 +3868,16 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
# Add ParamSection widgets from SECTIONS
|
# Add ParamSection widgets from SECTIONS
|
||||||
for section in SECTIONS:
|
for section in SECTIONS:
|
||||||
section_widget = ParamSection(section)
|
self.section_widget = ParamSection(section)
|
||||||
self.rows_layout.addWidget(section_widget)
|
self.rows_layout.addWidget(self.section_widget)
|
||||||
|
|
||||||
self.param_sections.append(section_widget)
|
self.param_sections.append(self.section_widget)
|
||||||
|
|
||||||
|
|
||||||
def clear_all(self):
|
def clear_all(self):
|
||||||
|
|
||||||
|
self.cancel_task()
|
||||||
|
|
||||||
self.right_column_widget.hide()
|
self.right_column_widget.hide()
|
||||||
|
|
||||||
# Clear the bubble layout
|
# Clear the bubble layout
|
||||||
@@ -3386,6 +3938,11 @@ class MainApplication(QMainWindow):
|
|||||||
self.help = UserGuideWindow(self)
|
self.help = UserGuideWindow(self)
|
||||||
self.help.show()
|
self.help.show()
|
||||||
|
|
||||||
|
def terminal_gui(self):
|
||||||
|
if self.terminal is None or not self.terminal.isVisible():
|
||||||
|
self.terminal = TerminalWindow(self)
|
||||||
|
self.terminal.show()
|
||||||
|
|
||||||
def update_optode_positions(self):
|
def update_optode_positions(self):
|
||||||
if self.optodes is None or not self.optodes.isVisible():
|
if self.optodes is None or not self.optodes.isVisible():
|
||||||
self.optodes = UpdateOptodesWindow(self)
|
self.optodes = UpdateOptodesWindow(self)
|
||||||
@@ -3415,6 +3972,12 @@ class MainApplication(QMainWindow):
|
|||||||
if folder_path:
|
if folder_path:
|
||||||
self.selected_path = folder_path # store the folder path
|
self.selected_path = folder_path # store the folder path
|
||||||
self.show_files_as_bubbles(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)
|
self.button1.setVisible(True)
|
||||||
|
|
||||||
@@ -3724,8 +4287,52 @@ class MainApplication(QMainWindow):
|
|||||||
return self.file_metadata
|
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'''
|
'''MODULE FILE'''
|
||||||
def on_run_task(self):
|
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
|
# Collect all selected snirf files in a flat list
|
||||||
snirf_files = []
|
snirf_files = []
|
||||||
|
|
||||||
@@ -4253,7 +4860,7 @@ def resource_path(relative_path):
|
|||||||
# PyInstaller bundle path
|
# PyInstaller bundle path
|
||||||
base_path = sys._MEIPASS
|
base_path = sys._MEIPASS
|
||||||
else:
|
else:
|
||||||
base_path = os.path.abspath(".")
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
return os.path.join(base_path, relative_path)
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user