From bdd5e1f5e17e4b68e4a353c5b28dd62e50f2a0d7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:41:58 +0000 Subject: [PATCH 1/2] Implement interactive row features in STUDY Dataset Editor - Implement 'clear_widgets' callback in QtDialogRenderer to reset row metadata. - Implement 'select_study_components' callback for ICA component selection. - Update pop_study_dialog_spec to use new callbacks for 'Clear' and 'All comp.' buttons. - Update _dataset_row_commands to include selected components in the STUDY history command. - Remove redundant pagination buttons as the dialog is already scrollable. - Ensure component count is correctly determined from ALLEEG metadata when available. Co-authored-by: suraj-ranganath <14310165+suraj-ranganath@users.noreply.github.com> --- src/eegprep/functions/guifunc/qt.py | 59 +++++++++++++++++ src/eegprep/functions/studyfunc/pop_study.py | 70 +++++++++++++------- 2 files changed, 105 insertions(+), 24 deletions(-) diff --git a/src/eegprep/functions/guifunc/qt.py b/src/eegprep/functions/guifunc/qt.py index c8eb6f8e..c2396bb4 100644 --- a/src/eegprep/functions/guifunc/qt.py +++ b/src/eegprep/functions/guifunc/qt.py @@ -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( @@ -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() @@ -1347,6 +1357,53 @@ 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: + for tag in params.get("targets", ()): + widget = widgets.get(tag) + if widget is None: + continue + if hasattr(widget, "setText"): + widget.setText("") + if hasattr(widget, "setProperty"): + widget.setProperty(_VALUE_PROPERTY, None) + + +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: @@ -1462,6 +1519,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: diff --git a/src/eegprep/functions/studyfunc/pop_study.py b/src/eegprep/functions/studyfunc/pop_study.py index cb4d233e..aa44c2d0 100644 --- a/src/eegprep/functions/studyfunc/pop_study.py +++ b/src/eegprep/functions/studyfunc/pop_study.py @@ -197,15 +197,37 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A ), ControlSpec( "pushbutton", - "All comp.", + _format_components_button(info.get("comps")), tag=f"dataset_{index}_components", - callback=_button_callback(coming_soon, f"dataset_{index}_components"), + value=info.get("comps") or [], + callback=CallbackSpec( + "select_study_components", + { + "button": f"dataset_{index}_components", + "count": _dataset_component_count(datasets, index), + "initial": info.get("comps") or [], + }, + ), ), 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", + f"dataset_{index}_components", + ], + }, + ), ), ) ) @@ -216,21 +238,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", @@ -241,7 +248,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), @@ -275,11 +282,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.", @@ -311,6 +313,8 @@ 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"))]) + if f"{prefix}components" in result: + commands.extend(["comps", result.get(f"{prefix}components")]) return commands @@ -345,4 +349,22 @@ 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 _format_components_button(comps: Any) -> str: + if not comps: + return "All comp." + if len(comps) > 3: + return f"Comp.: {' '.join(str(i) for i in comps[:2])} ..." + return f"Comp.: {' '.join(str(i) for i in comps)}" + + __all__ = ["pop_study", "pop_study_dialog_spec"] From 1d3e4aac7b68fc7477754a6c1e9714f275905ee7 Mon Sep 17 00:00:00 2001 From: Suraj Ranganath Date: Tue, 23 Jun 2026 23:25:04 -0700 Subject: [PATCH 2/2] Fix STUDY component selection commands --- src/eegprep/functions/guifunc/qt.py | 6 ++- src/eegprep/functions/studyfunc/pop_study.py | 44 +++++++++++----- .../functions/studyfunc/std_editset.py | 8 ++- tests/test_gui_pop_study.py | 50 +++++++++++++++++++ 4 files changed, 93 insertions(+), 15 deletions(-) diff --git a/src/eegprep/functions/guifunc/qt.py b/src/eegprep/functions/guifunc/qt.py index c2396bb4..2aed9e6a 100644 --- a/src/eegprep/functions/guifunc/qt.py +++ b/src/eegprep/functions/guifunc/qt.py @@ -1358,14 +1358,16 @@ def _show_help(_qt_widgets: Any, dialog: Any, spec: DialogSpec) -> None: 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("") + widget.setText(str(labels.get(tag, ""))) if hasattr(widget, "setProperty"): - widget.setProperty(_VALUE_PROPERTY, None) + widget.setProperty(_VALUE_PROPERTY, values.get(tag)) def _select_study_components(button: Any, widgets: Mapping[str, Any], params: Mapping[str, Any]) -> None: diff --git a/src/eegprep/functions/studyfunc/pop_study.py b/src/eegprep/functions/studyfunc/pop_study.py index aa44c2d0..6dd25fea 100644 --- a/src/eegprep/functions/studyfunc/pop_study.py +++ b/src/eegprep/functions/studyfunc/pop_study.py @@ -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)), @@ -197,15 +199,15 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A ), ControlSpec( "pushbutton", - _format_components_button(info.get("comps")), - tag=f"dataset_{index}_components", - value=info.get("comps") or [], + _format_components_button(component_values), + tag=components_tag, + value=component_values, callback=CallbackSpec( "select_study_components", { - "button": f"dataset_{index}_components", + "button": components_tag, "count": _dataset_component_count(datasets, index), - "initial": info.get("comps") or [], + "initial": component_values, }, ), ), @@ -224,8 +226,10 @@ def pop_study_dialog_spec(STUDY: dict[str, Any] | None, ALLEEG: list[dict[str, A f"dataset_{index}_run", f"dataset_{index}_condition", f"dataset_{index}_group", - f"dataset_{index}_components", + components_tag, ], + "labels": {components_tag: "All comp."}, + "values": {components_tag: []}, }, ), ), @@ -313,8 +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"))]) - if f"{prefix}components" in result: - commands.extend(["comps", result.get(f"{prefix}components")]) + components = result.get(f"{prefix}components") + if isinstance(components, list): + commands.extend(["comps", components]) return commands @@ -359,12 +364,27 @@ def _dataset_component_count(datasets: list[dict[str, Any]], index: int) -> int: 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: - if not comps: + values = _component_values(comps) + if not values: return "All comp." - if len(comps) > 3: - return f"Comp.: {' '.join(str(i) for i in comps[:2])} ..." - return f"Comp.: {' '.join(str(i) for i in comps)}" + 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"] diff --git a/src/eegprep/functions/studyfunc/std_editset.py b/src/eegprep/functions/studyfunc/std_editset.py index d17ac405..3dd645bb 100644 --- a/src/eegprep/functions/studyfunc/std_editset.py +++ b/src/eegprep/functions/studyfunc/std_editset.py @@ -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) @@ -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) diff --git a/tests/test_gui_pop_study.py b/tests/test_gui_pop_study.py index 77890094..76e499bf 100644 --- a/tests/test_gui_pop_study.py +++ b/tests/test_gui_pop_study.py @@ -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 @@ -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() @@ -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"})