LCOV - code coverage report
Current view: top level - src/pairinteraction_gui - settings.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 86 95 90.5 %
Date: 2026-08-14 15:26:44 Functions: 10 10 100.0 %

          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             :         # Values already handed to QSettings, keyed by their full path. Used to skip redundant writes,
      48             :         # since QSettings.setValue marks the store dirty even when the value did not change.
      49           1 :         self._written: dict[str, object] = {}
      50           1 :         self._dirty = False
      51             : 
      52           1 :     def value(self, key: str, default: object = None, value_type: type | None = None) -> object:
      53             :         """Read a stored value."""
      54           1 :         if value_type is not None:
      55           1 :             return self._settings.value(key, defaultValue=default, type=value_type)
      56           0 :         return self._settings.value(key, defaultValue=default)
      57             : 
      58           1 :     def set_value(self, key: str, value: object) -> None:
      59             :         """Store a value.
      60             : 
      61             :         Writing a value that is already stored is skipped, since QSettings.setValue marks the store dirty even when
      62             :         the value did not change, which would make the periodic autosave rewrite the whole ini file.
      63             :         """
      64             :         # Resolve `key` against the group that is currently open, as QSettings itself would.
      65           1 :         group = self._settings.group()
      66           1 :         full_key = f"{group}/{key}" if group else key
      67             : 
      68           1 :         if full_key in self._written and self._written[full_key] == value:
      69           1 :             return
      70           1 :         self._settings.setValue(key, value)
      71           1 :         self._written[full_key] = value
      72           1 :         self._dirty = True
      73             : 
      74           1 :     def sync(self) -> bool:
      75             :         """Flush pending changes to disk, if there are any.
      76             : 
      77             :         Returns True if there were unflushed changes (note that Qt may have flushed them already on its own).
      78             :         Skipping the flush when nothing changed keeps a periodic autosave from rewriting the whole ini file.
      79             :         """
      80           1 :         if not self._dirty:
      81           1 :             return False
      82           1 :         self._settings.sync()
      83           1 :         self._dirty = False
      84           1 :         return True
      85             : 
      86           1 :     def _get_mapper(self, widget: QWidget) -> tuple[str, str, type] | tuple[None, None, None]:
      87           1 :         return next((m for c, m in self.widget_mappers.items() if isinstance(widget, c)), (None, None, None))
      88             : 
      89           1 :     def update_widgets_from_settings(self, widget_map: dict[str, QWidget], *, combos_only: bool = False) -> None:
      90             :         """Set widget states from stored settings values."""
      91           1 :         for name, widget in widget_map.items():
      92           1 :             if combos_only and not isinstance(widget, QComboBox):
      93           1 :                 continue
      94             : 
      95           1 :             getter, setter, dtype = self._get_mapper(widget)
      96           1 :             if not getter:
      97           0 :                 continue
      98             : 
      99           1 :             value = getattr(widget, getter)()
     100           1 :             stored = self.value(name, value, dtype)
     101           1 :             if stored is None:
     102           0 :                 continue
     103             : 
     104           1 :             if setter:
     105           1 :                 try:
     106           1 :                     getattr(widget, setter)(stored)
     107           0 :                 except Exception as e:
     108           0 :                     logger.warning("Failed to restore setting '%s' with value '%s': %s", name, stored, e)
     109             : 
     110           1 :     def update_settings_from_widgets(self, widget_map: dict[str, QWidget]) -> None:
     111             :         """Save widget states into settings."""
     112           1 :         for name, widget in widget_map.items():
     113           1 :             getter, _setter, _dtype = self._get_mapper(widget)
     114           1 :             if getter:
     115           1 :                 value = getattr(widget, getter)()
     116           1 :                 if value is not None:
     117           1 :                     self.set_value(name, value)
     118             : 
     119           1 :     def save_widget_state(self, root: QWidget, group: str) -> None:
     120             :         """Write the current state of all named input widgets under `group`."""
     121           1 :         if not isinstance(root, BaseConfig):
     122           0 :             return
     123           1 :         widget_map = self.collect_widgets(root)
     124           1 :         self._settings.beginGroup(group)
     125           1 :         self.update_settings_from_widgets(widget_map)
     126           1 :         self._settings.endGroup()
     127             : 
     128           1 :     def restore_widget_state(self, root: QWidget, group: str) -> None:
     129             :         """Restore widget state (two-pass: combos first, then others)."""
     130           1 :         if not isinstance(root, BaseConfig):
     131           0 :             return
     132           1 :         widget_map = self.collect_widgets(root)
     133           1 :         self._settings.beginGroup(group)
     134           1 :         self.update_widgets_from_settings(widget_map, combos_only=True)
     135           1 :         widget_map = self.collect_widgets(root)
     136           1 :         self.update_widgets_from_settings(widget_map, combos_only=False)
     137           1 :         self._settings.endGroup()
     138             : 
     139           1 :     def collect_widgets(self, root: QWidget, widget_map: dict[str, QWidget] | None = None) -> dict[str, QWidget]:
     140           1 :         if widget_map is None:
     141           1 :             widget_map = {}
     142           1 :         for child in root.children():
     143           1 :             if not isinstance(child, QWidget):
     144           1 :                 continue
     145           1 :             name = child.objectName()
     146           1 :             if name and any(isinstance(child, c) for c in self.widget_mappers):
     147           1 :                 if name in widget_map:
     148           0 :                     logger.warning("Duplicate widget name '%s' found. Only the last one will be saved/restored.", name)
     149           1 :                 widget_map[name] = child
     150             : 
     151             :             # For stacked widgets, only recurse into the currently shown page
     152           1 :             if isinstance(child, QStackedWidget):
     153           1 :                 current = child.currentWidget()
     154           1 :                 if current is not None:
     155           1 :                     self.collect_widgets(current, widget_map)
     156             :             else:
     157           1 :                 self.collect_widgets(child, widget_map)
     158             : 
     159           1 :         return widget_map

Generated by: LCOV version 1.16