diff options
| -rw-r--r-- | core/app_settings.py | 53 | ||||
| -rw-r--r-- | ui/main_window.py | 4 | ||||
| -rw-r--r-- | ui/windows/settings_window.py | 28 |
3 files changed, 83 insertions, 2 deletions
diff --git a/core/app_settings.py b/core/app_settings.py new file mode 100644 index 0000000..e8bc7ae --- /dev/null +++ b/core/app_settings.py @@ -0,0 +1,53 @@ +""" +core/app_settings.py + +Platform-aware persistence for application settings. + +Stored independently of profiles so they survive profile switches and +reflect per-computer preferences (theme, font, paths, etc.). + +Locations: + Windows : %APPDATA%/labUI/settings.json + macOS : ~/Library/Application Support/labUI/settings.json + Linux : $XDG_CONFIG_HOME/labUI/settings.json (default ~/.config/labUI/) +""" + +import json +import os +import sys +from pathlib import Path + + +def _settings_path() -> Path: + if sys.platform == "win32": + base = Path(os.environ.get("APPDATA", Path.home())) + elif sys.platform == "darwin": + base = Path.home() / "Library" / "Application Support" + else: + base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) + return base / "labUI" / "settings.json" + + +SETTINGS_PATH = _settings_path() + + +def load_settings(defaults: dict) -> dict: + """Return defaults merged with any saved settings. Never raises.""" + cfg = dict(defaults) + try: + if SETTINGS_PATH.exists(): + with open(SETTINGS_PATH) as f: + cfg.update(json.load(f)) + except Exception: + pass + return cfg + + +def save_settings(cfg: dict) -> None: + """Persist cfg to disk. Never raises.""" + try: + SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(SETTINGS_PATH, "w") as f: + json.dump(cfg, f, indent=2) + except Exception: + pass diff --git a/ui/main_window.py b/ui/main_window.py index 7527a8a..ea299e7 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -45,6 +45,7 @@ from ui.windows.settings_window import SettingsWindow from plugins.plugin_manager import PluginManager from plugins.base_plugin import PluginContext +from core.app_settings import load_settings, save_settings _DARK_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_dark.qss") @@ -60,7 +61,7 @@ class MainWindow(QMainWindow): self.registry = DeviceRegistry() self.engine = AcquisitionEngine(poll_interval_ms=100) self.processor = SignalProcessor() - self._settings = dict(SettingsWindow._defaults) + self._settings = load_settings(SettingsWindow._defaults) self._elapsed = 0 self._win_devices = None @@ -548,6 +549,7 @@ class MainWindow(QMainWindow): def _on_settings(self, cfg: dict): self._settings.update(cfg) self.engine._interval = cfg.get("poll_ms", 100) / 1000.0 + save_settings(self._settings) def _tick(self): self._elapsed += 1 diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py index 422b6c9..1630efa 100644 --- a/ui/windows/settings_window.py +++ b/ui/windows/settings_window.py @@ -13,7 +13,7 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTabWidget, QFrame, QFormLayout, QComboBox, QSpinBox, QDoubleSpinBox, QCheckBox, QLineEdit, - QFileDialog, QScrollArea, QGroupBox, + QFileDialog, QScrollArea, QGroupBox, QMessageBox, ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QCloseEvent, QFont @@ -108,10 +108,25 @@ class SettingsWindow(QWidget): self._aa_chk = QCheckBox(); self._aa_chk.setChecked(self.cfg["antialias"]) lay.addRow("Anti-alias plots:", self._aa_chk) + rst = QPushButton("Reset to Defaults"); rst.setObjectName("configButton") + rst.clicked.connect(self._reset_to_defaults) + lay.addRow("", rst) + scroll.setWidget(cont) root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) return w + def _populate_from_cfg(self): + self._theme_cb.setCurrentText(self.cfg["theme"].title()) + self._font_sp.setValue(self.cfg["font_size"]) + self._aa_chk.setChecked(self.cfg["antialias"]) + self._poll_sp.setValue(self.cfg["poll_ms"]) + self._buf_sp.setValue(self.cfg["buffer_size"]) + self._log_edit.setText(self.cfg["log_dir"]) + self._tw_sp.setValue(self.cfg["time_window_s"]) + self._legend_chk.setChecked(self.cfg["show_legend"]) + self._grid_chk.setChecked(self.cfg["show_grid"]) + def _acquisition_tab(self): w = QWidget() scroll = QScrollArea(); scroll.setWidgetResizable(True) @@ -251,6 +266,17 @@ class SettingsWindow(QWidget): # ── Actions ─────────────────────────────────────────────────────────── + def _reset_to_defaults(self): + reply = QMessageBox.question( + self, "Reset Settings", + "Reset all settings to defaults?\nThis cannot be undone.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if reply == QMessageBox.StandardButton.Yes: + self.cfg = dict(self._defaults) + self._populate_from_cfg() + def _browse_log(self): path = QFileDialog.getExistingDirectory(self, "Select Log Directory") if path: |
