Skip to content

Commit caecef4

Browse files
committed
ui-state
1 parent a256f09 commit caecef4

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

src/pymmcore_widgets/config_presets/_config_groups_editor.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,26 @@ def _copy_named_obj(obj: T, new_name: str) -> T:
4343
return obj
4444

4545

46+
# State management for groupbox and property checkbox states
47+
class _GroupboxState:
48+
"""Stores the state of a groupbox and its contained property checkboxes."""
49+
50+
def __init__(self) -> None:
51+
self.groupbox_checked: bool = True
52+
# (device, property) -> checked
53+
self.property_states: dict[tuple[str, str], bool] = {}
54+
self.active_device: str = "" # For active camera/shutter
55+
56+
57+
class _PresetUIState:
58+
"""Stores UI state for all groupboxes in a specific preset."""
59+
60+
def __init__(self) -> None:
61+
self.light_path: _GroupboxState = _GroupboxState()
62+
self.camera: _GroupboxState = _GroupboxState()
63+
self.objective: _GroupboxState = _GroupboxState()
64+
65+
4666
class ConfigGroupsEditor(QWidget):
4767
"""Widget for managing configuration groups and presets.
4868
@@ -63,6 +83,10 @@ def __init__(
6383
) -> None:
6484
super().__init__(parent)
6585

86+
# State management for groupbox and property checkbox states
87+
# Key: (group_name, preset_name), Value: _PresetUIState
88+
self._ui_states: dict[tuple[str, str], _PresetUIState] = {}
89+
6690
self.groups = MapManager(
6791
ConfigGroup, clone_function=_copy_named_obj, parent=self, base_key="Group"
6892
)
@@ -120,6 +144,11 @@ def __init__(
120144
self._cam_group.valueChanged.connect(self._update_model_from_gui)
121145
self._obj_group.valueChanged.connect(self._update_model_from_gui)
122146

147+
# Connect groupbox toggle signals to save state when toggled
148+
self._light_path_group.toggled.connect(self._on_groupbox_toggled)
149+
self._cam_group.toggled.connect(self._on_groupbox_toggled)
150+
self._obj_group.toggled.connect(self._on_groupbox_toggled)
151+
123152
# Public API -------------------------------------------------------
124153

125154
def setData(self, data: Iterable[ConfigGroup]) -> None:
@@ -175,6 +204,15 @@ def currentSettings(self) -> Collection[Setting]:
175204
}
176205
)
177206

207+
if self._obj_group.isChecked():
208+
settings.update(
209+
{
210+
(dev, prop): val
211+
for dev, prop, val in self._obj_group.settings()
212+
if val not in ("", None)
213+
}
214+
)
215+
178216
return [Setting(k[0], k[1], v) for k, v in settings.items()]
179217

180218
def setCurrentSettings(self, settings: Iterable[Setting]) -> None:
@@ -219,6 +257,8 @@ def update_options_from_core(self, core: CMMCorePlus) -> None:
219257
# PRIVATE -----------------------------------------------------------
220258

221259
def _on_current_group_changed(self) -> None:
260+
# Save current state before switching
261+
self._save_current_state()
222262
with signals_blocked(self.presets):
223263
if config_group := self.groups.currentValue():
224264
self.presets.setRoot(config_group.presets)
@@ -234,11 +274,150 @@ def _selected_preset(self) -> ConfigPreset | None:
234274
def _update_gui_from_model(self) -> None:
235275
if preset := self._selected_preset():
236276
self.setCurrentSettings(preset.settings)
277+
# Restore the UI state for this preset
278+
self._restore_state()
237279

238280
def _update_model_from_gui(self) -> None:
239281
if preset := self._selected_preset():
240282
preset.settings = list(self.currentSettings())
241283

284+
# State management methods ----------------------------------------
285+
286+
def _get_preset_state_key(self) -> tuple[str, str] | None:
287+
"""Get the current state key (group_name, preset_name)."""
288+
group_name = self.groups.currentKey()
289+
preset_name = self.presets.currentKey()
290+
if group_name is not None and preset_name is not None:
291+
return (group_name, preset_name)
292+
return None
293+
294+
def _get_current_ui_state(self) -> _PresetUIState:
295+
"""Get or create the UI state for the current group/preset."""
296+
key = self._get_preset_state_key()
297+
if key is None:
298+
return _PresetUIState()
299+
return self._ui_states.setdefault(key, _PresetUIState())
300+
301+
def _save_current_state(self) -> None:
302+
"""Save the current UI state for the current group/preset."""
303+
key = self._get_preset_state_key()
304+
if key is None:
305+
return
306+
307+
ui_state = self._ui_states.setdefault(key, _PresetUIState())
308+
309+
# Save light path state
310+
ui_state.light_path.groupbox_checked = self._light_path_group.isChecked()
311+
ui_state.light_path.active_device = (
312+
self._light_path_group.active_shutter.currentText()
313+
)
314+
ui_state.light_path.property_states.clear()
315+
# Save all property checkbox states, regardless of groupbox state
316+
for row in range(self._light_path_group.props.rowCount()):
317+
item = self._light_path_group.props.item(row, 0)
318+
if item is not None:
319+
# Extract device and property from item text (format: "device-property")
320+
text = item.text()
321+
if "-" in text:
322+
parts = text.split("-", 1) # Split only on first dash
323+
if len(parts) == 2:
324+
dev, prop = parts
325+
ui_state.light_path.property_states[(dev, prop)] = (
326+
item.checkState() == Qt.CheckState.Checked
327+
)
328+
329+
# Save camera state
330+
ui_state.camera.groupbox_checked = self._cam_group.isChecked()
331+
ui_state.camera.active_device = self._cam_group.active_camera.currentText()
332+
ui_state.camera.property_states.clear()
333+
# Save all property checkbox states, regardless of groupbox state
334+
for row in range(self._cam_group.props.rowCount()):
335+
item = self._cam_group.props.item(row, 0)
336+
if item is not None:
337+
# Extract device and property from item text: "device-property"
338+
text = item.text()
339+
if "-" in text:
340+
parts = text.split("-", 1) # Split only on first dash
341+
if len(parts) == 2:
342+
dev, prop = parts
343+
ui_state.camera.property_states[(dev, prop)] = (
344+
item.checkState() == Qt.CheckState.Checked
345+
)
346+
347+
# Save objective state
348+
ui_state.objective.groupbox_checked = self._obj_group.isChecked()
349+
# Note: ObjectiveGroupBox doesn't have props yet, will need to add
350+
351+
def _restore_state(self) -> None:
352+
"""Restore the UI state for the current group/preset."""
353+
ui_state = self._get_current_ui_state()
354+
355+
# Block signals to prevent triggering valueChanged during restoration
356+
with (
357+
signals_blocked(self._light_path_group),
358+
signals_blocked(self._cam_group),
359+
signals_blocked(self._obj_group),
360+
):
361+
# Restore light path state
362+
self._light_path_group.setChecked(ui_state.light_path.groupbox_checked)
363+
if ui_state.light_path.active_device:
364+
self._light_path_group.active_shutter.setCurrentText(
365+
ui_state.light_path.active_device
366+
)
367+
368+
# Restore light path property checkboxes
369+
for row in range(self._light_path_group.props.rowCount()):
370+
item = self._light_path_group.props.item(row, 0)
371+
if item is not None:
372+
text = item.text()
373+
if "-" in text:
374+
parts = text.split("-", 1)
375+
if len(parts) == 2:
376+
dev, prop = parts
377+
if (dev, prop) in ui_state.light_path.property_states:
378+
checked = ui_state.light_path.property_states[
379+
(dev, prop)
380+
]
381+
check_state = (
382+
Qt.CheckState.Checked
383+
if checked
384+
else Qt.CheckState.Unchecked
385+
)
386+
item.setCheckState(check_state)
387+
388+
# Restore camera state
389+
self._cam_group.setChecked(ui_state.camera.groupbox_checked)
390+
if ui_state.camera.active_device:
391+
self._cam_group.active_camera.setCurrentText(
392+
ui_state.camera.active_device
393+
)
394+
395+
# Restore camera property checkboxes
396+
for row in range(self._cam_group.props.rowCount()):
397+
item = self._cam_group.props.item(row, 0)
398+
if item is not None:
399+
text = item.text()
400+
if "-" in text:
401+
parts = text.split("-", 1)
402+
if len(parts) == 2:
403+
dev, prop = parts
404+
if (dev, prop) in ui_state.camera.property_states:
405+
checked = ui_state.camera.property_states[(dev, prop)]
406+
check_state = (
407+
Qt.CheckState.Checked
408+
if checked
409+
else Qt.CheckState.Unchecked
410+
)
411+
item.setCheckState(check_state)
412+
413+
# Restore objective state
414+
self._obj_group.setChecked(ui_state.objective.groupbox_checked)
415+
416+
def _on_groupbox_toggled(self) -> None:
417+
"""Handle groupbox toggle events - save state and update model."""
418+
self._save_current_state()
419+
self._update_model_from_gui()
420+
242421

243422
def _is_not_objective(item: QTableWidgetItem, prop: DeviceProperty) -> bool:
244423
return not any(x in prop.device for x in prop.core.guessObjectiveDevices())
@@ -383,3 +562,9 @@ def __init__(self, parent: QWidget | None = None) -> None:
383562
layout = QVBoxLayout(self)
384563
layout.addWidget(self._obj_wdg)
385564
layout.setContentsMargins(12, 0, 12, 0)
565+
566+
def settings(self) -> Iterable[tuple[str, str, str]]:
567+
"""Return settings from the objectives widget."""
568+
# For now, return empty as ObjectivesWidget doesn't expose settings
569+
# This can be expanded when ObjectivesWidget is properly implemented
570+
return []

0 commit comments

Comments
 (0)