Line data Source code
1 : # SPDX-FileCopyrightText: 2026 PairInteraction Developers
2 : # SPDX-License-Identifier: LGPL-3.0-or-later
3 1 : from __future__ import annotations
4 :
5 1 : import logging
6 1 : from typing import TYPE_CHECKING, ClassVar
7 :
8 1 : from PySide6.QtCore import QObject, QSettings
9 1 : from PySide6.QtWidgets import (
10 : QCheckBox,
11 : QComboBox,
12 : QDoubleSpinBox,
13 : QRadioButton,
14 : QSpinBox,
15 : QStackedWidget,
16 : QWidget,
17 : )
18 :
19 1 : from pairinteraction import _backend
20 1 : from pairinteraction_gui.config.base_config import BaseConfig
21 :
22 : if TYPE_CHECKING:
23 : from pathlib import Path
24 :
25 :
26 1 : logger = logging.getLogger(__name__)
27 :
28 :
29 1 : class SettingsManager(QObject):
30 : """Settings manager."""
31 :
32 1 : widget_mappers: ClassVar[dict[type, tuple[str, str, type]]] = {
33 : QCheckBox: ("isChecked", "setChecked", bool),
34 : QSpinBox: ("value", "setValue", int),
35 : QDoubleSpinBox: ("value", "setValue", float),
36 : QRadioButton: ("isChecked", "setChecked", bool),
37 : QComboBox: ("currentText", "setCurrentText", str),
38 : }
39 :
40 1 : def __init__(self, cache_dir: Path | None = None) -> None:
41 1 : super().__init__()
42 1 : if cache_dir is None:
43 0 : cache_dir = _backend.get_cache_directory()
44 1 : path = cache_dir / "gui_settings.ini"
45 1 : path.parent.mkdir(parents=True, exist_ok=True)
46 1 : self.settings = QSettings(str(path), QSettings.Format.IniFormat)
47 :
48 1 : def value(self, key: str, default: object = None, value_type: type | None = None) -> object:
49 0 : if value_type is not None:
50 0 : return self.settings.value(key, defaultValue=default, type=value_type)
51 0 : return self.settings.value(key, defaultValue=default)
52 :
53 1 : def set_value(self, key: str, value: object) -> None:
54 0 : self.settings.setValue(key, value)
55 :
56 1 : def _get_mapper(self, widget: QWidget) -> tuple[str, str, type] | tuple[None, None, None]:
57 1 : return next((m for c, m in self.widget_mappers.items() if isinstance(widget, c)), (None, None, None))
58 :
59 1 : def update_widgets_from_settings(self, widget_map: dict[str, QWidget], *, combos_only: bool = False) -> None:
60 : """Set widget states from stored settings values."""
61 1 : for name, widget in widget_map.items():
62 1 : if combos_only and not isinstance(widget, QComboBox):
63 1 : continue
64 :
65 1 : getter, setter, dtype = self._get_mapper(widget)
66 1 : if not getter:
67 0 : continue
68 :
69 1 : value = getattr(widget, getter)()
70 1 : stored = self.settings.value(name, value, type=dtype)
71 1 : if stored is None:
72 0 : continue
73 :
74 1 : if setter:
75 1 : try:
76 1 : getattr(widget, setter)(stored)
77 0 : except Exception as e:
78 0 : logger.warning("Failed to restore setting '%s' with value '%s': %s", name, stored, e)
79 :
80 1 : def update_settings_from_widgets(self, widget_map: dict[str, QWidget]) -> None:
81 : """Save widget states into settings."""
82 1 : for name, widget in widget_map.items():
83 1 : getter, _setter, _dtype = self._get_mapper(widget)
84 1 : if getter:
85 1 : value = getattr(widget, getter)()
86 1 : if value is not None:
87 1 : self.settings.setValue(name, value)
88 :
89 1 : def save_widget_state(self, root: QWidget, group: str) -> None:
90 : """Write the current state of all named input widgets under `group`."""
91 1 : if not isinstance(root, BaseConfig):
92 0 : return
93 1 : widget_map = self.collect_widgets(root)
94 1 : self.settings.beginGroup(group)
95 1 : self.update_settings_from_widgets(widget_map)
96 1 : self.settings.endGroup()
97 :
98 1 : def restore_widget_state(self, root: QWidget, group: str) -> None:
99 : """Restore widget state (two-pass: combos first, then others)."""
100 1 : if not isinstance(root, BaseConfig):
101 0 : return
102 1 : widget_map = self.collect_widgets(root)
103 1 : self.settings.beginGroup(group)
104 1 : self.update_widgets_from_settings(widget_map, combos_only=True)
105 1 : widget_map = self.collect_widgets(root)
106 1 : self.update_widgets_from_settings(widget_map, combos_only=False)
107 1 : self.settings.endGroup()
108 :
109 1 : def collect_widgets(self, root: QWidget, widget_map: dict[str, QWidget] | None = None) -> dict[str, QWidget]:
110 1 : if widget_map is None:
111 1 : widget_map = {}
112 1 : for child in root.children():
113 1 : if not isinstance(child, QWidget):
114 1 : continue
115 1 : name = child.objectName()
116 1 : if name and any(isinstance(child, c) for c in self.widget_mappers):
117 1 : if name in widget_map:
118 0 : logger.warning("Duplicate widget name '%s' found. Only the last one will be saved/restored.", name)
119 1 : widget_map[name] = child
120 :
121 : # For stacked widgets, only recurse into the currently shown page
122 1 : if isinstance(child, QStackedWidget):
123 1 : current = child.currentWidget()
124 1 : if current is not None:
125 1 : self.collect_widgets(current, widget_map)
126 : else:
127 1 : self.collect_widgets(child, widget_map)
128 :
129 1 : return widget_map
|