mostly finish rewrite
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
Filename: exportcsv.py
|
||||||
|
Description: Export data as csv analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QFileDialog, QGridLayout, QHBoxLayout, QMessageBox, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
||||||
|
from PySide6.QtCore import QSize
|
||||||
|
|
||||||
|
from src.shared.flaresbasewidget import FlaresBaseWidget
|
||||||
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataAsCSVViewerWidget(FlaresBaseWidget):
|
||||||
|
def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
|
||||||
|
super().__init__("ExportDataAsCSVViewer")
|
||||||
|
self.setWindowTitle(f"Export Data As CSV Viewer - {APP_NAME.upper()}")
|
||||||
|
self.haemo_dict = haemo_dict
|
||||||
|
self.cha_dict = cha_dict
|
||||||
|
self.df_ind = df_ind
|
||||||
|
self.design_matrix = design_matrix
|
||||||
|
self.group = group
|
||||||
|
self.contrast_results_dict = contrast_results_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 (Export Data to CSV)",
|
||||||
|
"1 (CSV for SPARKS)",
|
||||||
|
# "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.generate_and_save_csv)
|
||||||
|
|
||||||
|
self.top_bar.addWidget(QLabel("Participants:"))
|
||||||
|
self.top_bar.addWidget(self.participant_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Export 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 generate_and_save_csv(self):
|
||||||
|
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not selected_file_paths or not selected_indexes:
|
||||||
|
QMessageBox.warning(self, "Selection Missing", "Please select at least one participant and one export type.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. ASK ONCE: Select Output Directory
|
||||||
|
output_dir = QFileDialog.getExistingDirectory(self, "Select Output Folder for CSV Exports")
|
||||||
|
|
||||||
|
if not output_dir:
|
||||||
|
print("Export cancelled: No folder selected.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
|
# Pass the necessary arguments to each method
|
||||||
|
for file_path in selected_file_paths:
|
||||||
|
base_filename = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
if idx == 0:
|
||||||
|
save_path = os.path.join(output_dir, f"{base_filename}_exported.csv")
|
||||||
|
if cha is not None:
|
||||||
|
cha.to_csv(save_path)
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
|
||||||
|
elif idx == 1:
|
||||||
|
# SPARKS Export
|
||||||
|
save_path = os.path.join(output_dir, f"{base_filename}_sparks.csv")
|
||||||
|
if haemo_obj is not None:
|
||||||
|
raw = haemo_obj
|
||||||
|
data, times = raw.get_data(return_times=True)
|
||||||
|
ann_col = np.full(times.shape, "", dtype=object)
|
||||||
|
|
||||||
|
if raw.annotations is not None and len(raw.annotations) > 0:
|
||||||
|
for onset, duration, desc in zip(
|
||||||
|
raw.annotations.onset,
|
||||||
|
raw.annotations.duration,
|
||||||
|
raw.annotations.description
|
||||||
|
):
|
||||||
|
mask = (times >= onset) & (times < onset + duration)
|
||||||
|
ann_col[mask] = desc
|
||||||
|
|
||||||
|
df = pd.DataFrame(data.T, columns=raw.ch_names)
|
||||||
|
df.insert(0, "annotation", ann_col)
|
||||||
|
df.insert(0, "time", times)
|
||||||
|
df.to_csv(save_path, index=False)
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"No method defined for index {idx}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to export {file_path} (Type {idx}): {e}")
|
||||||
|
|
||||||
|
# 4. Final Notification
|
||||||
|
if success_count > 0:
|
||||||
|
QMessageBox.information(self, "Export Complete", f"Successfully saved {success_count} CSV files to:\n{output_dir}")
|
||||||
|
|
||||||
|
# # If SPARKS export was included, show the Event Window once at the end
|
||||||
|
# if 1 in selected_indexes:
|
||||||
|
# win = UpdateEventsWindow(
|
||||||
|
# parent=self,
|
||||||
|
# mode=EventUpdateMode.WRITE_JSON,
|
||||||
|
# caller="Video Alignment Tool"
|
||||||
|
# )
|
||||||
|
# win.show()
|
||||||
|
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
"""
|
||||||
|
Filename: group.py
|
||||||
|
Description: Group analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
||||||
|
from PySide6.QtCore import QSize
|
||||||
|
|
||||||
|
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
|
||||||
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class GroupViewerWidget(FlaresBaseWidget):
|
||||||
|
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
|
||||||
|
super().__init__("GroupViewer")
|
||||||
|
self.setWindowTitle(f"Group Viewer - {APP_NAME.upper()}")
|
||||||
|
self.haemo_dict = haemo_dict
|
||||||
|
self.cha = cha
|
||||||
|
self.df_ind = df_ind
|
||||||
|
self.design_matrix = design_matrix
|
||||||
|
self.contrast_results = contrast_results
|
||||||
|
self.group = group
|
||||||
|
self.show_all_events = True
|
||||||
|
self._updating_checkstates = False
|
||||||
|
|
||||||
|
# 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.group_to_paths = {}
|
||||||
|
for file_path, group_name in self.group.items():
|
||||||
|
self.group_to_paths.setdefault(group_name, []).append(file_path)
|
||||||
|
|
||||||
|
self.group_names = sorted(self.group_to_paths.keys())
|
||||||
|
|
||||||
|
self.group_dropdown = QComboBox()
|
||||||
|
self.group_dropdown.addItem("<None Selected>")
|
||||||
|
self.group_dropdown.addItems(self.group_names)
|
||||||
|
self.group_dropdown.setCurrentIndex(0)
|
||||||
|
self.group_dropdown.currentIndexChanged.connect(self.update_participant_list_for_group)
|
||||||
|
|
||||||
|
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
|
||||||
|
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
|
||||||
|
self.participant_dropdown.setEnabled(False)
|
||||||
|
|
||||||
|
self.event_dropdown = QComboBox()
|
||||||
|
self.event_dropdown.addItem("<None Selected>")
|
||||||
|
|
||||||
|
self.index_texts = [
|
||||||
|
"0 (GLM Results)",
|
||||||
|
"1 (Significance)",
|
||||||
|
"2 (Brain Activity Visualization)",
|
||||||
|
# "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("Group:"))
|
||||||
|
self.top_bar.addWidget(self.group_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Participants:"))
|
||||||
|
self.top_bar.addWidget(self.participant_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Event:"))
|
||||||
|
self.top_bar.addWidget(self.event_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Image Indexes:"))
|
||||||
|
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 show_brain_images(self):
|
||||||
|
import flares as flares
|
||||||
|
|
||||||
|
selected_event = self.event_dropdown.currentText()
|
||||||
|
if selected_event == "<None Selected>":
|
||||||
|
selected_event = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if selected_event:
|
||||||
|
valid_paths = []
|
||||||
|
for fp in selected_file_paths:
|
||||||
|
raw = self.haemo_dict.get(fp)
|
||||||
|
# Check if this participant actually has the event in their annotations
|
||||||
|
if raw is not None and hasattr(raw, "annotations"):
|
||||||
|
if selected_event in raw.annotations.description:
|
||||||
|
valid_paths.append(fp)
|
||||||
|
|
||||||
|
selected_file_paths = valid_paths
|
||||||
|
|
||||||
|
selected_indexes = [
|
||||||
|
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not selected_file_paths:
|
||||||
|
print("No participants selected.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only keep indexes 0 and 1 that need parameters
|
||||||
|
parameterized_indexes = {
|
||||||
|
0: [
|
||||||
|
{
|
||||||
|
"key": "lower_bound",
|
||||||
|
"label": "Lower bound + <description>",
|
||||||
|
"default": "-0.3",
|
||||||
|
"type": float, # specify int here
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "upper_bound",
|
||||||
|
"label": "Upper bound + <description>",
|
||||||
|
"default": "0.8",
|
||||||
|
"type": float, # specify int here
|
||||||
|
}
|
||||||
|
],
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"key": "p_value",
|
||||||
|
"label": "Significance threshold P-value (e.g. 0.05)",
|
||||||
|
"default": "0.05",
|
||||||
|
"type": float,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "graph_bounds",
|
||||||
|
"label": "Graph Upper/Lower Limit",
|
||||||
|
"default": "3.0",
|
||||||
|
"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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
all_cha = pd.DataFrame()
|
||||||
|
for file_path in selected_file_paths:
|
||||||
|
haemo_obj = self.haemo_dict.get(file_path)
|
||||||
|
|
||||||
|
if selected_event:
|
||||||
|
participant_events = set(haemo_obj.annotations.description)
|
||||||
|
if selected_event not in participant_events:
|
||||||
|
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Pass the necessary arguments to each method
|
||||||
|
file_path = selected_file_paths[0]
|
||||||
|
p_haemo = self.haemo_dict.get(file_path)
|
||||||
|
p_design_matrix = self.design_matrix.get(file_path)
|
||||||
|
|
||||||
|
df_group = pd.DataFrame()
|
||||||
|
|
||||||
|
if selected_file_paths:
|
||||||
|
for file_path in selected_file_paths:
|
||||||
|
df = self.df_ind.get(file_path)
|
||||||
|
if df is not None:
|
||||||
|
df_group = pd.concat([df_group, df], ignore_index=True)
|
||||||
|
|
||||||
|
|
||||||
|
for idx in selected_indexes:
|
||||||
|
if idx == 0:
|
||||||
|
params = param_values.get(idx, {})
|
||||||
|
lower_bound = params.get("lower_bound", None)
|
||||||
|
upper_bound = params.get("upper_bound", None)
|
||||||
|
|
||||||
|
if lower_bound is None or upper_bound is None:
|
||||||
|
print(f"Missing parameters for index {idx}, skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
flares.plot_fir_model_results(df_group, p_haemo, p_design_matrix, selected_event, lower_bound, upper_bound)
|
||||||
|
|
||||||
|
elif idx == 1:
|
||||||
|
params = param_values.get(idx, {})
|
||||||
|
p_val = params.get("p_value", None)
|
||||||
|
graph_bounds = params.get("graph_bounds", None)
|
||||||
|
|
||||||
|
if p_val is None or graph_bounds is None:
|
||||||
|
print(f"Missing parameters for index {idx}, skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_contrasts = []
|
||||||
|
for fp in selected_file_paths:
|
||||||
|
condition_dfs = self.contrast_results.get(fp, {})
|
||||||
|
if selected_event in condition_dfs:
|
||||||
|
df = condition_dfs[selected_event].copy()
|
||||||
|
df["ID"] = fp
|
||||||
|
all_contrasts.append(df)
|
||||||
|
|
||||||
|
if not all_contrasts:
|
||||||
|
print("No contrast data found for selected participants and event.")
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
|
||||||
|
|
||||||
|
if len(selected_file_paths) > 1:
|
||||||
|
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
|
||||||
|
processed_raw = flares.aggregate_fnirs_group_geometry(raw_list)
|
||||||
|
else:
|
||||||
|
processed_raw = raw_list[0].copy().pick(picks="hbo")
|
||||||
|
|
||||||
|
flares.brain_3d_visualization(processed_raw, 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
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"No method defined for index {idx}")
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
"""
|
||||||
|
Filename: groupbrain.py
|
||||||
|
Description: Group brain analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
||||||
|
from PySide6.QtCore import QSize
|
||||||
|
|
||||||
|
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
|
||||||
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class GroupBrainViewerWidget(FlaresBaseWidget):
|
||||||
|
def __init__(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict):
|
||||||
|
super().__init__("GroupBrainViewer")
|
||||||
|
self.setWindowTitle(f"Group Brain Viewer - {APP_NAME.upper()}")
|
||||||
|
self.haemo_dict = haemo_dict
|
||||||
|
self.df_ind = df_ind
|
||||||
|
self.design_matrix = design_matrix
|
||||||
|
self.group = group
|
||||||
|
self.contrast_results_dict = contrast_results_dict
|
||||||
|
|
||||||
|
self.group_to_paths = {}
|
||||||
|
for file_path, group_name in self.group.items():
|
||||||
|
self.group_to_paths.setdefault(group_name, []).append(file_path)
|
||||||
|
|
||||||
|
self.group_names = sorted(self.group_to_paths.keys())
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.top_bar = QHBoxLayout()
|
||||||
|
self.layout.addLayout(self.top_bar)
|
||||||
|
|
||||||
|
|
||||||
|
self.group_a_dropdown = QComboBox()
|
||||||
|
self.group_a_dropdown.addItem("<None Selected>")
|
||||||
|
self.group_a_dropdown.addItems(self.group_names)
|
||||||
|
self.group_a_dropdown.currentIndexChanged.connect(self._update_group_a_options)
|
||||||
|
|
||||||
|
|
||||||
|
self.group_b_dropdown = QComboBox()
|
||||||
|
self.group_b_dropdown.addItem("<None Selected>")
|
||||||
|
self.group_b_dropdown.addItems(self.group_names)
|
||||||
|
self.group_b_dropdown.currentIndexChanged.connect(self._update_group_b_options)
|
||||||
|
|
||||||
|
|
||||||
|
self.event_dropdown = QComboBox()
|
||||||
|
self.event_dropdown.addItem("<None Selected>")
|
||||||
|
|
||||||
|
self.participant_dropdown_a = self._create_multiselect_dropdown([])
|
||||||
|
self.participant_dropdown_a.lineEdit().setPlaceholderText("Select participants (Group A)")
|
||||||
|
self.participant_dropdown_a.model().itemChanged.connect(self._on_participants_changed)
|
||||||
|
|
||||||
|
|
||||||
|
self.participant_dropdown_b = self._create_multiselect_dropdown([])
|
||||||
|
self.participant_dropdown_b.lineEdit().setPlaceholderText("Select participants (Group B)")
|
||||||
|
self.participant_dropdown_b.model().itemChanged.connect(self._on_participants_changed)
|
||||||
|
|
||||||
|
|
||||||
|
self.index_texts = [
|
||||||
|
"0 (Contrast Image)",
|
||||||
|
# "1 (3D Brain Contrast)",
|
||||||
|
# "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("Group A:"))
|
||||||
|
self.top_bar.addWidget(self.group_a_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Participants (Group A):"))
|
||||||
|
self.top_bar.addWidget(self.participant_dropdown_a)
|
||||||
|
self.top_bar.addWidget(QLabel("Group B:"))
|
||||||
|
self.top_bar.addWidget(self.group_b_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Participants (Group B):"))
|
||||||
|
self.top_bar.addWidget(self.participant_dropdown_b)
|
||||||
|
self.top_bar.addWidget(QLabel("Event:"))
|
||||||
|
self.top_bar.addWidget(self.event_dropdown)
|
||||||
|
self.top_bar.addWidget(QLabel("Image Indexes:"))
|
||||||
|
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 _update_group_b_options(self):
|
||||||
|
"""Triggered when Group B changes: Update Group A to exclude B's choice"""
|
||||||
|
selected_b = self.group_b_dropdown.currentText()
|
||||||
|
|
||||||
|
# Refresh Group A and exclude what was just picked in Group B
|
||||||
|
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected_b)
|
||||||
|
|
||||||
|
# Update the participants for Group B
|
||||||
|
self.update_participant_list_for_group(selected_b, self.participant_dropdown_b)
|
||||||
|
self._update_event_dropdown()
|
||||||
|
|
||||||
|
def _update_group_a_options(self):
|
||||||
|
"""Triggered when Group A changes: Update Group B to exclude A's choice"""
|
||||||
|
selected_a = self.group_a_dropdown.currentText()
|
||||||
|
|
||||||
|
# Refresh Group B and exclude what was just picked in Group A
|
||||||
|
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected_a)
|
||||||
|
|
||||||
|
# Update the participants for Group A
|
||||||
|
self.update_participant_list_for_group(selected_a, self.participant_dropdown_a)
|
||||||
|
self._update_event_dropdown()
|
||||||
|
|
||||||
|
def _on_participants_changed(self, item=None):
|
||||||
|
self._update_event_dropdown()
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_group_dropdown(self, dropdown, exclude):
|
||||||
|
current = dropdown.currentText()
|
||||||
|
dropdown.blockSignals(True)
|
||||||
|
dropdown.clear()
|
||||||
|
dropdown.addItem("<None Selected>")
|
||||||
|
for group in self.group_names:
|
||||||
|
if group != exclude:
|
||||||
|
dropdown.addItem(group)
|
||||||
|
# Restore previous selection if still valid
|
||||||
|
if current != "<None Selected>" and current != exclude and dropdown.findText(current) != -1:
|
||||||
|
dropdown.setCurrentText(current)
|
||||||
|
else:
|
||||||
|
dropdown.setCurrentIndex(0) # Reset to "<None Selected>"
|
||||||
|
dropdown.blockSignals(False)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_file_paths_from_labels(self, labels, group_name):
|
||||||
|
file_paths = []
|
||||||
|
|
||||||
|
if group_name == self.group_a_dropdown.currentText():
|
||||||
|
participant_map = self.participant_map_a
|
||||||
|
elif group_name == self.group_b_dropdown.currentText():
|
||||||
|
participant_map = self.participant_map_b
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Reverse map: display label -> file path
|
||||||
|
reverse_map = {
|
||||||
|
f"{label} ({os.path.basename(fp)})": fp
|
||||||
|
for fp, label in participant_map.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for label in labels:
|
||||||
|
file_path = reverse_map.get(label)
|
||||||
|
if file_path:
|
||||||
|
file_paths.append(file_path)
|
||||||
|
|
||||||
|
return file_paths
|
||||||
|
|
||||||
|
def show_brain_images(self):
|
||||||
|
import flares as flares
|
||||||
|
|
||||||
|
selected_event = self.event_dropdown.currentText()
|
||||||
|
if selected_event == "<None Selected>":
|
||||||
|
selected_event = None
|
||||||
|
|
||||||
|
# Group A
|
||||||
|
participants_a = self._get_checked_items(self.participant_dropdown_a)
|
||||||
|
file_paths_a = self._get_file_paths_from_labels(participants_a, self.group_a_dropdown.currentText())
|
||||||
|
|
||||||
|
# Group B
|
||||||
|
participants_b = self._get_checked_items(self.participant_dropdown_b)
|
||||||
|
file_paths_b = self._get_file_paths_from_labels(participants_b, self.group_b_dropdown.currentText())
|
||||||
|
|
||||||
|
selected_indexes = [
|
||||||
|
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
|
||||||
|
]
|
||||||
|
|
||||||
|
all_selected_paths = list(set(file_paths_a + file_paths_b))
|
||||||
|
|
||||||
|
if not all_selected_paths:
|
||||||
|
print("No participants selected.")
|
||||||
|
return
|
||||||
|
|
||||||
|
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": "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 about the contrast.",
|
||||||
|
"default": "True",
|
||||||
|
"type": bool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "brain_bounds",
|
||||||
|
"label": "Graph Upper/Lower Limit",
|
||||||
|
"default": "1.0",
|
||||||
|
"type": float,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "is_3d",
|
||||||
|
"label": "Should we display the results in a 3D interactive window?",
|
||||||
|
"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
|
||||||
|
|
||||||
|
# Build group-level contrast DataFrames
|
||||||
|
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
|
||||||
|
group_df = pd.DataFrame()
|
||||||
|
for fp in file_paths:
|
||||||
|
print(f"Looking up contrast for: {fp}")
|
||||||
|
event_con_dict = self.contrast_results_dict.get(fp, {})
|
||||||
|
print("Available events for this file:", list(event_con_dict.keys()))
|
||||||
|
if event and event in event_con_dict:
|
||||||
|
df = event_con_dict[event]
|
||||||
|
print(f"Appending contrast df for event: {event}")
|
||||||
|
group_df = pd.concat([group_df, df], ignore_index=True)
|
||||||
|
else:
|
||||||
|
print(f"Event '{event}' not found for {fp}")
|
||||||
|
return group_df
|
||||||
|
|
||||||
|
print("Selected event:", selected_event)
|
||||||
|
print("File paths A:", file_paths_a)
|
||||||
|
print("File paths B:", file_paths_b)
|
||||||
|
|
||||||
|
contrast_df_a = concat_group_contrasts(file_paths_a, selected_event)
|
||||||
|
contrast_df_b = concat_group_contrasts(file_paths_b, selected_event)
|
||||||
|
|
||||||
|
print("contrast_df_a empty?", contrast_df_a.empty)
|
||||||
|
print("contrast_df_b empty?", contrast_df_b.empty)
|
||||||
|
|
||||||
|
all_raw_objs = [self.haemo_dict.get(fp) for fp in all_selected_paths if self.haemo_dict.get(fp)]
|
||||||
|
|
||||||
|
if len(all_raw_objs) > 1:
|
||||||
|
processed_raw = flares.aggregate_fnirs_group_geometry(all_raw_objs)
|
||||||
|
else:
|
||||||
|
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
|
||||||
|
|
||||||
|
# Visualizations
|
||||||
|
for idx in selected_indexes:
|
||||||
|
if idx == 0:
|
||||||
|
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)
|
||||||
|
is_3d = params.get("is_3d", None)
|
||||||
|
|
||||||
|
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None or is_3d is None:
|
||||||
|
print(f"Missing parameters for index {idx}, skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not contrast_df_a.empty and not contrast_df_b.empty and processed_raw:
|
||||||
|
|
||||||
|
flares.plot_2d_3d_contrasts_between_groups(
|
||||||
|
contrast_df_a,
|
||||||
|
contrast_df_b,
|
||||||
|
raw_haemo=processed_raw,
|
||||||
|
group_a_name=self.group_a_dropdown.currentText(),
|
||||||
|
group_b_name=self.group_b_dropdown.currentText(),
|
||||||
|
is_3d=is_3d,
|
||||||
|
t_or_theta=t_or_theta,
|
||||||
|
show_optodes=show_optodes,
|
||||||
|
show_text=show_text,
|
||||||
|
brain_bounds=brain_bounds
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("no")
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: groupfunctionalconnectivity.py
|
||||||
|
Description: Group functional connectivity analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QMessageBox
|
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QMessageBox
|
||||||
@@ -10,14 +18,14 @@ from src.shared.shareddata import APP_NAME
|
|||||||
class GroupFunctionalConnectivityWidget(FlaresBaseWidget):
|
class GroupFunctionalConnectivityWidget(FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, group, config_dict):
|
def __init__(self, haemo_dict, group, config_dict):
|
||||||
super().__init__("GroupFunctionalConnectivityWidget")
|
super().__init__("GroupFunctionalConnectivityWidget")
|
||||||
self.setWindowTitle(f"{APP_NAME.upper} Group Viewer")
|
self.setWindowTitle(f"Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.group = group
|
self.group = group
|
||||||
self.config_dict = config_dict
|
self.config_dict = config_dict
|
||||||
self.show_all_events = True
|
self.show_all_events = True
|
||||||
self._updating_checkstates = False
|
self._updating_checkstates = False
|
||||||
|
|
||||||
QMessageBox.warning(self, f"Warning - {APP_NAME.upper}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
||||||
"By clicking OK, you accept that the images generated may not be factual.")
|
"By clicking OK, you accept that the images generated may not be factual.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: participant.py
|
||||||
|
Description: Participant analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -14,7 +22,7 @@ class ParticipantViewerWidget(FlaresBaseWidget):
|
|||||||
def __init__(self, haemo_dict, fig_bytes_dict):
|
def __init__(self, haemo_dict, fig_bytes_dict):
|
||||||
super().__init__("ParticipantViewer")
|
super().__init__("ParticipantViewer")
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||||
self.setWindowTitle(f"{APP_NAME.upper} Participant Viewer")
|
self.setWindowTitle(f"Participant Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.fig_bytes_dict = fig_bytes_dict
|
self.fig_bytes_dict = fig_bytes_dict
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: participantbrain.py
|
||||||
|
Description: Participant brain analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
||||||
@@ -10,7 +18,7 @@ from src.shared.shareddata import APP_NAME
|
|||||||
class ParticipantBrainViewerWidget(FlaresBaseWidget):
|
class ParticipantBrainViewerWidget(FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha_dict):
|
def __init__(self, haemo_dict, cha_dict):
|
||||||
super().__init__("ParticipantBrainViewer")
|
super().__init__("ParticipantBrainViewer")
|
||||||
self.setWindowTitle(f"{APP_NAME.upper} Participant Brain Viewer")
|
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.cha_dict = cha_dict
|
self.cha_dict = cha_dict
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: participantfunctionalconnectivity.py
|
||||||
|
Description: Participant functional connectivity analysis window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QMessageBox
|
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QMessageBox
|
||||||
@@ -10,11 +18,11 @@ from src.shared.shareddata import APP_NAME
|
|||||||
class ParticipantFunctionalConnectivityWidget(FlaresBaseWidget):
|
class ParticipantFunctionalConnectivityWidget(FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, epochs_dict):
|
def __init__(self, haemo_dict, epochs_dict):
|
||||||
super().__init__("FunctionalConnectivityWidget")
|
super().__init__("FunctionalConnectivityWidget")
|
||||||
self.setWindowTitle(f"{APP_NAME.upper} Functional Connectivity Viewer [BETA]")
|
self.setWindowTitle(f"Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.epochs_dict = epochs_dict
|
self.epochs_dict = epochs_dict
|
||||||
|
|
||||||
QMessageBox.warning(self, f"Warning - {APP_NAME.upper}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
||||||
"By clicking OK, you accept that the images generated may not be factual.")
|
"By clicking OK, you accept that the images generated may not be factual.")
|
||||||
|
|
||||||
# Create mappings: file_path -> participant label and dropdown display text
|
# Create mappings: file_path -> participant label and dropdown display text
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: flaresbasewidget.py
|
||||||
|
Description: Custom window design and supporting methods for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QVBoxLayout, QWidget, QFrame, QSpinBox
|
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QVBoxLayout, QWidget, QFrame, QSpinBox
|
||||||
@@ -5,7 +13,26 @@ from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValida
|
|||||||
from PySide6.QtCore import QEvent, Qt
|
from PySide6.QtCore import QEvent, Qt
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
from src.shared.flaresbasewidget import FullClickComboBox
|
|
||||||
|
|
||||||
|
class FullClickComboBox(QComboBox):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setEditable(True)
|
||||||
|
self.lineEdit().setReadOnly(True)
|
||||||
|
self.lineEdit().installEventFilter(self)
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
if obj == self.lineEdit():
|
||||||
|
|
||||||
|
if event.type() == QEvent.MouseButtonPress:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if event.type() == QEvent.MouseButtonRelease:
|
||||||
|
self.showPopup()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
|
||||||
class ClickableLabel(QLabel):
|
class ClickableLabel(QLabel):
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: shareddata.py
|
||||||
|
Description: Shared constants and methods for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -7,6 +15,8 @@ APP_NAME = "flares"
|
|||||||
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
||||||
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
||||||
PLATFORM_NAME = platform.system().lower()
|
PLATFORM_NAME = platform.system().lower()
|
||||||
|
CHANGELOG_URL = "https://git.research.dezeeuw.ca/tyler/flares/raw/branch/main/changelog_major.md"
|
||||||
|
WIKI_URL = "https://git.research.dezeeuw.ca/tyler/flares/wiki"
|
||||||
|
|
||||||
PIPELINE_STAGES = [
|
PIPELINE_STAGES = [
|
||||||
"Preprocessing",
|
"Preprocessing",
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: about.py
|
||||||
|
Description: About window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: terminal.py
|
||||||
|
Description: Terminal window for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: updateevents.py
|
||||||
|
Description: Methods to update snirf events for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
@@ -26,7 +34,7 @@ class UpdateEventsWindow(QWidget):
|
|||||||
|
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.caller = caller or self.__class__.__name__
|
self.caller = caller or self.__class__.__name__
|
||||||
self.setWindowTitle("Update event markers")
|
self.setWindowTitle(f"Update event markers - {APP_NAME.upper()}")
|
||||||
self.resize(760, 200)
|
self.resize(760, 200)
|
||||||
|
|
||||||
print("INIT MODE:", mode)
|
print("INIT MODE:", mode)
|
||||||
@@ -845,5 +853,3 @@ class UpdateEventsBlazesWindow(QWidget):
|
|||||||
f"Aligned {len(onsets)} events.\n(Filtered out {skipped_count} short events)")
|
f"Aligned {len(onsets)} events.\n(Filtered out {skipped_count} short events)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Filename: updateoptodes.py
|
||||||
|
Description: Methods to update optode locations for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -9,6 +17,7 @@ from PySide6.QtCore import Qt
|
|||||||
|
|
||||||
from mne.io import read_raw_snirf
|
from mne.io import read_raw_snirf
|
||||||
from mne_nirs.io import write_raw_snirf
|
from mne_nirs.io import write_raw_snirf
|
||||||
|
from mne.channels import make_dig_montage
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
@@ -17,7 +26,7 @@ class UpdateOptodesWindow(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent, Qt.WindowType.Window)
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
self.setWindowTitle("Update optode positions")
|
self.setWindowTitle(f"Update optode positions - {APP_NAME.upper()}")
|
||||||
self.resize(760, 200)
|
self.resize(760, 200)
|
||||||
|
|
||||||
self.label_file_a = QLabel("SNIRF file:")
|
self.label_file_a = QLabel("SNIRF file:")
|
||||||
@@ -136,7 +145,7 @@ class UpdateOptodesWindow(QWidget):
|
|||||||
|
|
||||||
def show_help_popup(self, text):
|
def show_help_popup(self, text):
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper}")
|
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
|
||||||
msg.setText(text)
|
msg.setText(text)
|
||||||
msg.exec()
|
msg.exec()
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -1,7 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Filename: userguide.py
|
||||||
|
Description: User guide for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME, PIPELINE_STAGES
|
from src.shared.shareddata import APP_NAME, PIPELINE_STAGES, WIKI_URL
|
||||||
|
|
||||||
|
|
||||||
class UserGuideWindow(QWidget):
|
class UserGuideWindow(QWidget):
|
||||||
@@ -22,7 +30,7 @@ class UserGuideWindow(QWidget):
|
|||||||
label2_text = "\n".join(f"Stage {idx + 1}: {name}" for idx, name in enumerate(PIPELINE_STAGES)) + "\n"
|
label2_text = "\n".join(f"Stage {idx + 1}: {name}" for idx, name in enumerate(PIPELINE_STAGES)) + "\n"
|
||||||
label2 = QLabel(label2_text, self)
|
label2 = QLabel(label2_text, self)
|
||||||
|
|
||||||
label3 = QLabel(f"For more information, visit the Git wiki page <a href='https://git.research.dezeeuw.ca/tyler/{APP_NAME}/wiki'>here</a>.", self)
|
label3 = QLabel(f"For more information, visit the Git wiki page <a href='{WIKI_URL}'>here</a>.", self)
|
||||||
label3.setTextFormat(Qt.TextFormat.RichText)
|
label3.setTextFormat(Qt.TextFormat.RichText)
|
||||||
label3.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
label3.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||||
label3.setOpenExternalLinks(True)
|
label3.setOpenExternalLinks(True)
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
Filename: viewerlauncher.py
|
||||||
|
Description: Analysis options launcher for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QPushButton, QWidget, QVBoxLayout
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
|
||||||
|
from src.analysis.exportcsv import ExportDataAsCSVViewerWidget
|
||||||
|
from src.analysis.group import GroupViewerWidget
|
||||||
|
from src.analysis.groupbrain import GroupBrainViewerWidget
|
||||||
|
from src.analysis.groupfunctionalconnectivity import GroupFunctionalConnectivityWidget
|
||||||
|
from src.analysis.participant import ParticipantViewerWidget
|
||||||
|
from src.analysis.participantbrain import ParticipantBrainViewerWidget
|
||||||
|
from src.analysis.participantfoldchannels import ParticipantFoldChannelsWidget
|
||||||
|
from src.analysis.participantfunctionalconnectivity import ParticipantFunctionalConnectivityWidget
|
||||||
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class ViewerLauncherWidget(QWidget):
|
||||||
|
def __init__(self, haemo_dict, config_dict, fig_bytes_dict, cha_dict, contrast_results_dict, df_ind, design_matrix, epochs_dict, folding_bypass):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}")
|
||||||
|
|
||||||
|
group_dict = {
|
||||||
|
file_path: config.get("GROUP", "Unknown")
|
||||||
|
for file_path, config in config_dict.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def launch(func, btn, *args):
|
||||||
|
func(*args)
|
||||||
|
self._trigger_success(btn)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
btn1 = QPushButton("Open Participant Viewer")
|
||||||
|
btn1.clicked.connect(lambda: launch(self.open_participant_viewer, btn1, haemo_dict, fig_bytes_dict))
|
||||||
|
btn1.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
btn2 = QPushButton("Open Participant Brain Viewer")
|
||||||
|
btn2.clicked.connect(lambda: launch(self.open_participant_brain_viewer, btn2, haemo_dict, cha_dict))
|
||||||
|
btn2.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
btn3 = QPushButton("Open Participant Fold Channels Viewer")
|
||||||
|
btn3.clicked.connect(lambda: launch(self.open_participant_fold_channels_viewer, btn3, haemo_dict, cha_dict))
|
||||||
|
|
||||||
|
btn7 = QPushButton("Open Functional Connectivity Viewer [BETA]")
|
||||||
|
btn7.clicked.connect(lambda: launch(self.open_participant_functional_connectivity_viewer, btn7, haemo_dict, epochs_dict))
|
||||||
|
btn7.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
btn8 = QPushButton("Open Group Functional Connectivity Viewer [BETA]")
|
||||||
|
btn8.clicked.connect(lambda: launch(self.open_group_functional_connectivity_viewer, btn8, haemo_dict, group_dict, config_dict))
|
||||||
|
btn8.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
btn4 = QPushButton("Open Inter-Group Viewer")
|
||||||
|
btn4.clicked.connect(lambda: launch(self.open_group_viewer, btn4, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group_dict))
|
||||||
|
btn4.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
btn5 = QPushButton("Open Cross Group Brain Viewer")
|
||||||
|
btn5.clicked.connect(lambda: launch(self.open_group_brain_viewer, btn5, haemo_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
|
||||||
|
btn5.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
btn6 = QPushButton("Open Export Data As CSV Viewer")
|
||||||
|
btn6.clicked.connect(lambda: launch(self.open_export_data_as_csv_viewer, btn6, haemo_dict, cha_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
|
||||||
|
btn6.setEnabled(not folding_bypass)
|
||||||
|
|
||||||
|
layout.addWidget(btn1)
|
||||||
|
layout.addWidget(btn2)
|
||||||
|
layout.addWidget(btn3)
|
||||||
|
layout.addWidget(btn7)
|
||||||
|
layout.addWidget(btn8)
|
||||||
|
layout.addWidget(btn4)
|
||||||
|
layout.addWidget(btn5)
|
||||||
|
layout.addWidget(btn6)
|
||||||
|
|
||||||
|
def open_participant_viewer(self, haemo_dict, fig_bytes_dict):
|
||||||
|
self.participant_viewer = ParticipantViewerWidget(haemo_dict, fig_bytes_dict)
|
||||||
|
self.participant_viewer.show()
|
||||||
|
|
||||||
|
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_participant_functional_connectivity_viewer(self, haemo_dict, epochs_dict):
|
||||||
|
self.participant_brain_viewer = ParticipantFunctionalConnectivityWidget(haemo_dict, epochs_dict)
|
||||||
|
self.participant_brain_viewer.show()
|
||||||
|
|
||||||
|
def open_group_functional_connectivity_viewer(self, haemo_dict, group, config_dict):
|
||||||
|
self.participant_brain_viewer = GroupFunctionalConnectivityWidget(haemo_dict, group, config_dict)
|
||||||
|
self.participant_brain_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)
|
||||||
|
self.participant_brain_viewer.show()
|
||||||
|
|
||||||
|
def open_group_brain_viewer(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict):
|
||||||
|
self.participant_brain_viewer = GroupBrainViewerWidget(haemo_dict, df_ind, design_matrix, group, contrast_results_dict)
|
||||||
|
self.participant_brain_viewer.show()
|
||||||
|
|
||||||
|
def open_export_data_as_csv_viewer(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
|
||||||
|
self.export_data_as_csv_viewer = ExportDataAsCSVViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict)
|
||||||
|
self.export_data_as_csv_viewer.show()
|
||||||
|
|
||||||
|
def _trigger_success(self, button):
|
||||||
|
"""Temporarily adds a green checkmark to the button text."""
|
||||||
|
original_text = button.text()
|
||||||
|
button.setText(f"{original_text} ✔")
|
||||||
|
button.setStyleSheet("color: green; font-weight: bold;")
|
||||||
|
|
||||||
|
# Revert after 1 second
|
||||||
|
QTimer.singleShot(1000, lambda: self._revert_button(button, original_text))
|
||||||
|
|
||||||
|
def _revert_button(self, button, original_text):
|
||||||
|
button.setText(original_text)
|
||||||
|
button.setStyleSheet("")
|
||||||
+18
-12
@@ -1,9 +1,17 @@
|
|||||||
|
"""
|
||||||
|
Filename: welcome.py
|
||||||
|
Description: Welcome dialog for FLARES
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
from PySide6.QtWidgets import QTextBrowser, QVBoxLayout, QLabel, QDialog, QHBoxLayout, QPushButton
|
from PySide6.QtWidgets import QTextBrowser, QVBoxLayout, QLabel, QDialog, QHBoxLayout, QPushButton
|
||||||
from PySide6.QtGui import QDesktopServices, QIcon
|
from PySide6.QtGui import QDesktopServices, QIcon
|
||||||
from PySide6.QtCore import QUrl
|
from PySide6.QtCore import QUrl
|
||||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, resource_path
|
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, CHANGELOG_URL, resource_path
|
||||||
|
|
||||||
|
|
||||||
class WelcomeDialog(QDialog):
|
class WelcomeDialog(QDialog):
|
||||||
@@ -13,13 +21,13 @@ class WelcomeDialog(QDialog):
|
|||||||
self.setMinimumSize(550, 450)
|
self.setMinimumSize(550, 450)
|
||||||
self.resize(800, 500)
|
self.resize(800, 500)
|
||||||
|
|
||||||
# Main Layout
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
# Header Layout (Logo + App Name)
|
|
||||||
header_layout = QHBoxLayout()
|
header_layout = QHBoxLayout()
|
||||||
logo_label = QLabel(self)
|
logo_label = QLabel(self)
|
||||||
logo_label.setPixmap(QIcon(resource_path("icons/main.ico")).pixmap(48, 48)) # Fits cleanly in a header
|
|
||||||
|
# NOTE: might not work on mac and need the icns file
|
||||||
|
logo_label.setPixmap(QIcon(resource_path("icons/main.ico")).pixmap(48, 48))
|
||||||
if direct:
|
if direct:
|
||||||
title_label = QLabel(f"<h2>{APP_NAME.upper()} has been sucessfully updated to version {CURRENT_VERSION}!</h2>", self)
|
title_label = QLabel(f"<h2>{APP_NAME.upper()} has been sucessfully updated to version {CURRENT_VERSION}!</h2>", self)
|
||||||
else:
|
else:
|
||||||
@@ -30,19 +38,19 @@ class WelcomeDialog(QDialog):
|
|||||||
header_layout.addStretch()
|
header_layout.addStretch()
|
||||||
layout.addLayout(header_layout)
|
layout.addLayout(header_layout)
|
||||||
|
|
||||||
# Text Browser Area (Automatically converts Markdown syntax into clean formatted UI text)
|
|
||||||
self.text_browser = QTextBrowser(self)
|
self.text_browser = QTextBrowser(self)
|
||||||
self.text_browser.setHtml("<p style='color: gray;'>Loading latest updates from server...</p>")
|
self.text_browser.setHtml("<p style='color: gray;'>Loading latest updates from server...</p>")
|
||||||
self.text_browser.setOpenLinks(False) # Don't open links inside the viewer
|
|
||||||
|
# Ensure links open in the default web browser and not in this window
|
||||||
|
self.text_browser.setOpenLinks(False)
|
||||||
self.text_browser.anchorClicked.connect(QDesktopServices.openUrl)
|
self.text_browser.anchorClicked.connect(QDesktopServices.openUrl)
|
||||||
layout.addWidget(self.text_browser)
|
layout.addWidget(self.text_browser)
|
||||||
|
|
||||||
# Footer Controls Layout
|
|
||||||
footer_layout = QHBoxLayout()
|
footer_layout = QHBoxLayout()
|
||||||
|
|
||||||
ok_button = QPushButton("OK", self)
|
ok_button = QPushButton("OK", self)
|
||||||
ok_button.setDefault(True)
|
ok_button.setDefault(True)
|
||||||
ok_button.clicked.connect(self.accept) # Closes the dialog with a success signal
|
ok_button.clicked.connect(self.accept)
|
||||||
|
|
||||||
footer_layout.addStretch()
|
footer_layout.addStretch()
|
||||||
footer_layout.addWidget(ok_button)
|
footer_layout.addWidget(ok_button)
|
||||||
@@ -51,18 +59,16 @@ class WelcomeDialog(QDialog):
|
|||||||
# Fetch markdown from the web asynchronously
|
# Fetch markdown from the web asynchronously
|
||||||
self.network_manager = QNetworkAccessManager(self)
|
self.network_manager = QNetworkAccessManager(self)
|
||||||
self.network_manager.finished.connect(self._on_download_complete)
|
self.network_manager.finished.connect(self._on_download_complete)
|
||||||
|
self.network_manager.get(QNetworkRequest(QUrl(CHANGELOG_URL)))
|
||||||
md_url = "https://git.research.dezeeuw.ca/tyler/flares/raw/branch/main/changelog_major.md"
|
|
||||||
self.network_manager.get(QNetworkRequest(QUrl(md_url)))
|
|
||||||
|
|
||||||
|
|
||||||
def _on_download_complete(self, reply):
|
def _on_download_complete(self, reply):
|
||||||
"""Processes the downloaded markdown and drops it into the view frame."""
|
"""Processes the downloaded markdown and drops it into the view frame."""
|
||||||
if reply.error() == reply.NetworkError.NoError:
|
if reply.error() == reply.NetworkError.NoError:
|
||||||
raw_bytes = reply.readAll()
|
raw_bytes = reply.readAll()
|
||||||
|
|
||||||
# Convert raw bytes to standard text string
|
# Convert raw bytes to standard text string
|
||||||
markdown_text = str(raw_bytes, encoding='utf-8')
|
markdown_text = str(raw_bytes, encoding='utf-8')
|
||||||
# Qt's QTextBrowser natively renders markdown arrays beautifully!
|
|
||||||
self.text_browser.setMarkdown(markdown_text)
|
self.text_browser.setMarkdown(markdown_text)
|
||||||
else:
|
else:
|
||||||
self.text_browser.setHtml(
|
self.text_browser.setHtml(
|
||||||
|
|||||||
Reference in New Issue
Block a user