Source code for spacr.qt.prefs
"""Per-user persistence of recent paths in spaCR's canonical Qt store.
Recent paths used to live under two legacy ``Olafsson Lab`` namespaces.
The first access copies their keys into ``QSettings("spacr", "qt")`` without
deleting or overwriting either legacy store, so upgrades and downgrades both
remain safe.
"""
from __future__ import annotations
from threading import RLock
from typing import List
from PySide6.QtCore import QSettings
ORG = "spacr"
APP = "qt"
_LEGACY_NAMESPACES = (
("Olafsson Lab", "spaCR"),
("Olafsson Lab", "SpaCR"),
)
_MIGRATED_FILES: set[str] = set()
_MIGRATION_LOCK = RLock()
def _migrate_legacy_settings(current: QSettings) -> None:
"""Copy missing keys from both historical recent-path stores once.
Existing values in the canonical store win: they were written by newer
code and must not be replaced if an older spaCR build subsequently writes
to a legacy store. The old stores are read only and deliberately remain
intact so downgrading cannot lose a user's recent paths.
"""
current_file = str(current.fileName())
with _MIGRATION_LOCK:
if current_file in _MIGRATED_FILES:
return
for organization, application in _LEGACY_NAMESPACES:
legacy = QSettings(*_store_args(organization, application))
if str(legacy.fileName()) == current_file:
continue
legacy.sync()
for key in legacy.allKeys():
if not current.contains(key):
current.setValue(key, legacy.value(key))
current.sync()
_MIGRATED_FILES.add(current_file)
def _store_args(organization: str, application: str) -> tuple:
"""Return the ``QSettings`` constructor arguments for one store.
Normally ``(organization, application)``, the platform's native store.
In portable mode an INI file in the portable settings folder instead, on
every platform, so the registry on Windows and the preferences plists on
macOS are left untouched.
"""
from ..logging_util import _portable_settings_dir
folder = _portable_settings_dir()
if folder is None:
return (organization, application)
QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, str(folder))
return (QSettings.IniFormat, QSettings.UserScope, organization,
application)
def _s() -> QSettings:
"""Return spaCR's canonical store after a non-destructive migration."""
settings = QSettings(*_store_args(ORG, APP))
_migrate_legacy_settings(settings)
return settings
[docs]
def get_last_source(app_key: str) -> str:
"""Return the last folder used for a given app, or '' if unknown.
:param app_key: the app key; the value is stored under
``recent/<app_key>/`` in the Qt settings.
"""
v = _s().value(f"recent/{app_key}/last")
return str(v) if v else ""
[docs]
def set_last_source(app_key: str, path: str) -> None:
"""Remember ``path`` as the most-recent source folder for ``app_key``.
:param app_key: the app key; the value is stored under
``recent/<app_key>/`` in the Qt settings.
:param path: the folder to remember; an empty path is ignored.
"""
if not path:
return
settings = _s()
settings.setValue(f"recent/{app_key}/last", path)
[docs]
def get_recent_sources(app_key: str, limit: int = 8) -> List[str]:
"""Return the recent-source list (most-recent first).
:param app_key: the app key; the value is stored under
``recent/<app_key>/`` in the Qt settings.
"""
v = _s().value(f"recent/{app_key}/list")
if isinstance(v, str):
items = [p for p in v.split("\n") if p]
elif isinstance(v, list):
items = [str(p) for p in v if p]
else:
items = []
return items[:limit]
[docs]
def push_recent_source(app_key: str, path: str, limit: int = 8) -> None:
"""Insert `path` at the head of the recent list and de-duplicate.
:param app_key: the app key; the value is stored under
``recent/<app_key>/`` in the Qt settings.
:param path: the folder to put first; an empty path is ignored. It also
becomes the last source.
"""
if not path:
return
items = [p for p in get_recent_sources(app_key, limit=limit + 1) if p != path]
items.insert(0, path)
_s().setValue(f"recent/{app_key}/list", "\n".join(items[:limit]))
set_last_source(app_key, path)