Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/eegprep/functions/guifunc/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class QtDialogRenderer:
_set_reref_mode: Any
_set_enabled: Any
_show_help: Any
_clear_widgets: Any
_select_study_components: Any
_read_widget: Any

def run(
Expand Down Expand Up @@ -363,6 +365,14 @@ def _connect_callback(self, callback: CallbackSpec | None, widgets: dict[str, An
source = widgets.get(params["source"])
if source is not None:
source.clicked.connect(lambda: self._navigate_channel(widgets, params))
elif callback.name == "clear_widgets":
source = widgets.get(params["button"])
if source is not None:
source.clicked.connect(lambda: self._clear_widgets(widgets, params))
elif callback.name == "select_study_components":
button = widgets.get(params["button"])
if button is not None:
button.clicked.connect(lambda: self._select_study_components(button, widgets, params))

def _run_tf_cycle_calc(self, button: Any, widgets: dict[str, Any], params: Mapping[str, Any]) -> None:
_qt_core, qt_widgets = _require_qt()
Expand Down Expand Up @@ -1347,6 +1357,55 @@ def _show_help(_qt_widgets: Any, dialog: Any, spec: DialogSpec) -> None:
dialog._eegprep_help_dialog = pophelp(spec.help_text or spec.function_name, parent=dialog)


def _clear_widgets(widgets: Mapping[str, Any], params: Mapping[str, Any]) -> None:
labels = params.get("labels", {})
values = params.get("values", {})
for tag in params.get("targets", ()):
widget = widgets.get(tag)
if widget is None:
continue
if hasattr(widget, "setText"):
widget.setText(str(labels.get(tag, "")))
if hasattr(widget, "setProperty"):
widget.setProperty(_VALUE_PROPERTY, values.get(tag))


def _select_study_components(button: Any, widgets: Mapping[str, Any], params: Mapping[str, Any]) -> None:
count = int(params.get("count", 0))
if count <= 0:
_qt_core, qt_widgets = _require_qt()
qt_widgets.QMessageBox.warning(button, "Warning", "No components found for this dataset. Run ICA first.")
return

labels = [f"IC {i + 1}" for i in range(count)]
current = button.property(_VALUE_PROPERTY) or params.get("initial", [])
if isinstance(current, str):
initial = current
else:
initial = " ".join(str(i) for i in current)

chanlist, chanliststr, _allchanstr = pop_chansel(
labels,
withindex="on",
select=initial,
parent=button,
)

if chanlist is None:
return

button.setProperty(_VALUE_PROPERTY, list(chanlist))

if not chanlist:
button.setText("All comp.")
else:
if len(chanlist) > 3:
label = f"Comp.: {' '.join(str(i) for i in chanlist[:2])} ..."
else:
label = f"Comp.: {' '.join(str(i) for i in chanlist)}"
button.setText(label)


def _read_widget(widget: Any) -> Any:
stored_value = widget.property(_VALUE_PROPERTY)
if stored_value is not None:
Expand Down Expand Up @@ -1462,6 +1521,8 @@ def _firpm_default_devs(amplitudes: list[float]) -> list[float]:
"_set_reref_mode",
"_set_enabled",
"_show_help",
"_clear_widgets",
"_select_study_components",
"_read_widget",
)
for _helper_name in _QT_RENDERER_STATIC_HELPERS:
Expand Down
92 changes: 67 additions & 25 deletions src/eegprep/functions/studyfunc/pop_study.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A
]
for index in range(1, visible_rows + 1):
info = datasetinfo[index - 1] if index <= len(datasetinfo) else {}
component_values = _component_values(info.get("comps"))
browse_tag = f"dataset_{index}_browse"
filename_tag = f"dataset_{index}_filename"
components_tag = f"dataset_{index}_components"
controls.extend(
(
ControlSpec("text", str(index)),
Expand Down Expand Up @@ -197,15 +199,39 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A
),
ControlSpec(
"pushbutton",
"All comp.",
tag=f"dataset_{index}_components",
callback=_button_callback(coming_soon, f"dataset_{index}_components"),
_format_components_button(component_values),
tag=components_tag,
value=component_values,
callback=CallbackSpec(
"select_study_components",
{
"button": components_tag,
"count": _dataset_component_count(datasets, index),
"initial": component_values,
},
),
),
ControlSpec(
"pushbutton",
"Clear",
tag=f"dataset_{index}_clear",
callback=_button_callback(coming_soon, f"dataset_{index}_clear"),
callback=CallbackSpec(
"clear_widgets",
{
"button": f"dataset_{index}_clear",
"targets": [
filename_tag,
f"dataset_{index}_subject",
f"dataset_{index}_session",
f"dataset_{index}_run",
f"dataset_{index}_condition",
f"dataset_{index}_group",
components_tag,
],
"labels": {components_tag: "All comp."},
"values": {components_tag: []},
},
),
),
)
)
Expand All @@ -216,21 +242,6 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A
"Important note: Removed datasets will not be saved before being deleted from EEGPrep memory",
),
ControlSpec("spacer"),
ControlSpec(
"pushbutton",
"<",
tag="previous_page",
callback=_button_callback(coming_soon, "previous_page"),
),
ControlSpec("text", "Page 1"),
ControlSpec(
"pushbutton",
">",
tag="next_page",
callback=_button_callback(coming_soon, "next_page"),
),
ControlSpec("spacer"),
ControlSpec("spacer"),
ControlSpec("checkbox", "", tag="delete_cluster_info", value=False),
ControlSpec(
"text",
Expand All @@ -241,7 +252,7 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A
header_geometry = (0.2, 1.05, 0.35, 0.4, 0.35, 0.25, 0.6, 0.4, 0.6, 0.3)
geometry = [(1,), (0.2, 1, 3.5), (0.2, 1, 3.5), (0.2, 1, 3.5), (1,), header_geometry]
geometry.extend(header_geometry for _index in range(visible_rows))
geometry.extend(((1,), (1, 0.2, 0.3, 0.2, 1), (1,), (0.14, 3)))
geometry.extend(((1,), (1,), (0.14, 3)))
return DialogSpec(
title=title,
controls=tuple(controls),
Expand Down Expand Up @@ -275,11 +286,6 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A
min-width: 76px;
max-width: 76px;
}
QDialog#pop_study QPushButton#previous_page,
QDialog#pop_study QPushButton#next_page {
min-width: 46px;
max-width: 46px;
}
""",
known_differences=(
"EEGPrep Phase 5a edits loaded dataset metadata; dataset browsing is provided by pop_studywizard.",
Expand Down Expand Up @@ -311,6 +317,9 @@ def _dataset_row_commands(datasets: list[dict[str, Any]], result: dict[str, Any]
commands.extend(["session", parse_optional_int_text(result.get(f"{prefix}session"))])
if f"{prefix}run" in result:
commands.extend(["run", parse_optional_int_text(result.get(f"{prefix}run"))])
components = result.get(f"{prefix}components")
if isinstance(components, list):
commands.extend(["comps", components])
return commands


Expand Down Expand Up @@ -345,4 +354,37 @@ def _button_callback(template: CallbackSpec, button: str) -> CallbackSpec:
return CallbackSpec(template.name, params, template.matlab_callback)


def _dataset_component_count(datasets: list[dict[str, Any]], index: int) -> int:
if index > len(datasets):
return 0
eeg = datasets[index - 1]
weights = eeg.get("icaweights")
if weights is not None and hasattr(weights, "shape"):
return int(weights.shape[0])
return 0


def _component_values(comps: Any) -> list[Any]:
if comps is None:
return []
if isinstance(comps, str):
return comps.split()
if hasattr(comps, "ravel") and hasattr(comps, "tolist"):
return list(comps.ravel().tolist())
if hasattr(comps, "tolist"):
comps = comps.tolist()
if isinstance(comps, (list, tuple, set)):
return list(comps)
return [comps]


def _format_components_button(comps: Any) -> str:
values = _component_values(comps)
if not values:
return "All comp."
if len(values) > 3:
return f"Comp.: {' '.join(str(i) for i in values[:2])} ..."
return f"Comp.: {' '.join(str(i) for i in values)}"


__all__ = ["pop_study", "pop_study_dialog_spec"]
8 changes: 7 additions & 1 deletion src/eegprep/functions/studyfunc/std_editset.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def _flatten_commands(commands: Any) -> list[Any]:
for item in commands:
if isinstance(item, dict):
flat.extend(_flatten_commands(item))
elif isinstance(item, (list, tuple)):
elif _is_command_sequence(item):
flat.extend(_flatten_commands(list(item)))
else:
flat.append(item)
Expand All @@ -136,6 +136,12 @@ def _flatten_commands(commands: Any) -> list[Any]:
return flat


def _is_command_sequence(value: Any) -> bool:
if not isinstance(value, (list, tuple)):
return False
return len(value) % 2 == 0 and all(isinstance(value[index], str) for index in range(0, len(value), 2))


def _metadata_value(key: str, value: Any) -> Any:
if key in {"session", "run"}:
return parse_optional_int_text(value)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_gui_pop_study.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ast

import numpy as np

from eegprep.functions.guifunc.spec import controls_by_tag
from eegprep.functions.studyfunc.pop_study import pop_study, pop_study_dialog_spec
from eegprep.functions.studyfunc.pop_studydesign import pop_studydesign, pop_studydesign_dialog_spec
Expand Down Expand Up @@ -84,6 +86,43 @@ def test_pop_study_gui_updates_metadata_and_returns_python_history():
assert namespace["ALLEEG"][0]["condition"] == "standard"


def test_pop_study_gui_ignores_untouched_components_button_label():
study, alleeg = _study_inputs()
renderer = _Renderer(
{
"name": "Edited",
"task": "",
"notes": "",
"dataset_1_subject": "S02",
"dataset_1_components": "All comp.",
}
)

edited, _edited_alleeg, command = pop_study(study, alleeg, gui=True, renderer=renderer, return_com=True)

assert edited["datasetinfo"][0]["subject"] == "S02"
assert "comps" not in command
ast.parse(command)


def test_pop_study_gui_records_selected_components():
study, alleeg = _study_inputs()
renderer = _Renderer(
{
"name": "Study",
"task": "",
"notes": "",
"dataset_1_components": [1, 2],
}
)

edited, _edited_alleeg, command = pop_study(study, alleeg, gui=True, renderer=renderer, return_com=True)

assert edited["datasetinfo"][0]["comps"] == [1, 2]
assert "'comps', [1, 2]" in command
ast.parse(command)


def test_pop_study_gui_cancel_is_noop():
study, alleeg = _study_inputs()

Expand All @@ -94,6 +133,17 @@ def test_pop_study_gui_cancel_is_noop():
assert command == ""


def test_pop_study_dialog_spec_handles_numpy_component_lists():
study, alleeg = _study_inputs()
study["datasetinfo"][0]["comps"] = np.array([1, 2, 3, 4])

spec = pop_study_dialog_spec(study, alleeg)
controls = controls_by_tag(spec)

assert controls["dataset_1_components"].string == "Comp.: 1 2 ..."
assert controls["dataset_1_components"].value == [1, 2, 3, 4]


def test_pop_studydesign_dialog_spec_lists_factors_and_current_design():
first = create_test_eeg(n_channels=2, n_samples=10)
first.update({"setname": "one", "subject": "S01", "condition": "target", "filename": "one.set"})
Expand Down
Loading