@@ -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+
4666class 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
243422def _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