Source code for gemviz.user_settings

"""
Interface to support user-specific application settings.

Use this for remembering:

* window positions and geometry
* user preferences


The name of the settings file is given in the main window.
Note, the settings file may have the suffix ``.ini`` on some operating systems.
Remove the settings file to clear any settings.

.. TODO: There is also a menu item to clear this file and reset it to defaults.

This module uses ``QtCore.QSettings``.
(https://doc.qt.io/qtforpython-5/PySide2/QtCore/QSettings.html#qsettings)

..  note:: Multi-monitor support : method restoreWindowGeometry()

    On multi-monitor systems such as laptops, window may be
    restored to offscreen position.  Here is how it happens:

    * geo was saved while window was on 2nd screen while docked
    * now re-opened on laptop display and window is off-screen

    For now, keep the windows on the main screen
    or learn how to edit the settings file.

:see: https://github.com/prjemian/assign_gup/blob/master/src/Assign_GUP/settings.py

.. autosummary::

    ~ApplicationQSettings
    ~settings
"""

import datetime
import logging

from PyQt5 import QtCore
from PyQt5 import QtWidgets

from .__init__ import __package_name__
from .__init__ import __settings_orgName__

GLOBAL_GROUP = "___global___"
logger = logging.getLogger(__name__)
settings = None  # Edge case during Sphinx documentation.


[docs] class ApplicationQSettings(QtCore.QSettings): """ Manage and preserve settings for this application using QSettings. Use the .ini file format and save under user directory. .. autosummary:: ~to_dict ~init_global_keys ~_keySplit_ ~keyExists ~getKey ~setKey ~resetDefaults ~updateTimeStamp ~saveWindowGeometry ~restoreWindowGeometry ~saveSplitter ~restoreSplitter """ def __init__(self, *args): if len(args) != 2: # Edge case during Sphinx documentation. return settings # return the singleton organization, application = args super().__init__( # self, QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, organization, application, ) self.init_global_keys() def __repr__(self): keys = "fileName applicationName organizationName status".split() d = {k: getattr(self, k)() for k in keys} d.update(self.to_dict()) dl = ", ".join([f"{k}={v!r}" for k, v in d.items()]) return f"{self.__class__.__name__}({dl})"
[docs] def to_dict(self): """Return a dict with all the settings.""" return {k: self.getKey(k) for k in self.allKeys()}
def init_global_keys(self): logger.debug("fileName=%s", self.fileName) d = dict( this_file=self.fileName(), # the .ini file, that is version=1.0, # timestamp=str(datetime.datetime.now()), ) for k, v in d.items(): key = f"{GLOBAL_GROUP}/{k}" if self.getKey(key) in ("", None): self.setValue(key, v)
[docs] def _keySplit_(self, full_key): """ split full_key into (group, key) tuple :param str full_key: either `key` or `group/key`, default group (unspecified) is GLOBAL_GROUP """ if len(full_key) == 0: raise KeyError("must supply a key") parts = full_key.split("/") if len(parts) > 2: raise KeyError('too many "/" separators: ' + full_key) if len(parts) == 1: group, key = GLOBAL_GROUP, str(parts[0]) elif len(parts) == 2: group, key = map(str, parts) return group, key
[docs] def keyExists(self, key): """does the named key exist?""" return key in self.allKeys()
[docs] def getKey(self, key): """ Return the Python object of key or None if not found. """ if "/" not in key and not self.keyExists(key): key = f"{GLOBAL_GROUP}/{key}" return self.value(key)
[docs] def setKey(self, key, value): """ Set the value of a configuration key, creates the key if it does not exist. :param str key: either `key` or `group/key` Complement: self.value(key) returns value of key """ # ?WHY? if not self.keyExists(key): group, k = self._keySplit_(key) if group is None: group = GLOBAL_GROUP self.remove(key) self.beginGroup(group) self.setValue(k, value) self.endGroup() if key != "timestamp": self.updateTimeStamp()
[docs] def resetDefaults(self): """ Reset all application settings to default values. """ for key in self.allKeys(): self.remove(key) self.init_global_keys()
def updateTimeStamp(self): """ """ self.setKey("timestamp", str(datetime.datetime.now()))
[docs] def saveWindowGeometry(self, window, label): """ Remember the window's location. :param obj window: instance of QWidget :param str label: group name to use in settings file """ geo = window.geometry() self.setKey(f"{label}/x", geo.x()) self.setKey(f"{label}/y", geo.y()) self.setKey(f"{label}/width", geo.width()) self.setKey(f"{label}/height", geo.height())
[docs] def restoreWindowGeometry(self, window, label): """ Put the window back in place. :param obj window: instance of QWidget :param str label: group name to use in settings file """ width = self.getKey(f"{label}/width") height = self.getKey(f"{label}/height") if width is None or height is None: return window.resize(QtCore.QSize(int(width), int(height))) x = self.getKey(f"{label}/x") y = self.getKey(f"{label}/y") if x is None or y is None: return # is this window on any available screen? qdw = QtWidgets.QDesktopWidget() x_onscreen = False y_onscreen = False for screen_num in range(qdw.screenCount()): # find the "available" screen dimensions # (excludes docks, menu bars, ...) available_rect = qdw.availableGeometry(screen_num) if ( available_rect.x() <= int(x) < available_rect.x() + available_rect.width() ): x_onscreen = True if ( available_rect.y() <= int(y) < available_rect.y() + available_rect.height() ): y_onscreen = True # Move the window to the primary window if it would otherwise be drawn off screen available_rect = qdw.availableGeometry(qdw.primaryScreen()) if not x_onscreen: offset = available_rect.x() + available_rect.width() / 10 x = available_rect.x() + offset width = min(int(width), available_rect.width()) if not y_onscreen: offset = available_rect.y() + available_rect.height() / 10 y = available_rect.y() + offset height = min(int(height), available_rect.height()) window.setGeometry(QtCore.QRect(int(x), int(y), int(width), int(height)))
[docs] def saveSplitter(self, splitter, label): """ remember where the splitter was :param obj splitter: instance of QSplitter :param str label: group name to use in settings file """ sizes = map(int, splitter.sizes()) self.setKey(f"{label}/sizes", " ".join(map(str, sizes)))
[docs] def restoreSplitter(self, splitter, label): """ put the splitter back where it was :param obj splitter: instance of QSplitter :param str label: group name to use in settings file """ sizes = self.getKey(f"{label}/sizes") if sizes is not None: splitter.setSizes(map(int, str(sizes).split()))
settings = ApplicationQSettings(__settings_orgName__, __package_name__) """Application settings object (a singleton)."""