Source code for stratified_packager.gui.wdg_plugin_options_page

"""Plugin settings form integrated into QGIS 'Options' menu."""

from __future__ import annotations

import logging
import platform
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, override
from urllib.parse import quote

from qgis.core import Qgis, QgsApplication, QgsProject
from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory
from qgis.PyQt import uic
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtGui import QDesktopServices, QIcon
from qgis.PyQt.QtWidgets import QGridLayout, QLabel

from stratified_packager.__about__ import (
    __title__,
    __uri_homepage__,
    __uri_tracker__,
    __version__,
)
from stratified_packager.identity import plugin_icon
from stratified_packager.settings import StratifiedPackagerSettings
from stratified_packager.toolbelt.logging import QgisLoggerWrapper, Target
from stratified_packager.toolbelt.settings import ProjectVariables

from .widgets import (
    FieldKind,
    FieldSpec,
    concrete_value,
    default_fields,
    make_concrete_editor,
    set_concrete_value,
)

if TYPE_CHECKING:
    from qgis.PyQt.QtWidgets import (
        QCheckBox,
        QGroupBox,
        QPushButton,
        QVBoxLayout,
        QWidget,
    )

log = QgisLoggerWrapper.get_logger(__name__)

# uic.loadUiType has no type stubs; it returns dynamically-generated classes.
FORM_CLASS, _ = uic.loadUiType(Path(__file__).with_suffix(".ui"))  # type: ignore[no-untyped-call]


# FORM_CLASS is a dynamically-generated form class — opaque to static checkers.
[docs] class PluginOptionsPageWidget(QgsOptionsPageWidget, FORM_CLASS): # type: ignore[misc,valid-type] """Settings form embedded into QGIS 'options' menu.""" verticalLayout: QVBoxLayout # noqa: N815 gridLayout: QGridLayout # noqa: N815 grp_misc: QGroupBox grp_defaults: QGroupBox defaults_host: QWidget lbl_title: QLabel lbl_version_saved_value: QLabel lbl_version_saved: QLabel btn_report: QPushButton btn_help: QPushButton btn_reset: QPushButton opt_debug: QCheckBox
[docs] @override def __init__(self, parent: QWidget | None = None) -> None: """ Initialize settings dialog. :param parent: base class for widgets for pages included in the options dialog. """ super().__init__(parent) self.setupUi(self) self.plg_settings = StratifiedPackagerSettings() self._default_fields: tuple[FieldSpec, ...] = default_fields() self._default_editors: dict[str, QWidget] = {} self._default_notes: dict[str, QLabel] = {} self._build_defaults_editors() self.initGui()
[docs] def _build_defaults_editors(self) -> None: """Build a concrete editor and an override-note label for each default field.""" grid = QGridLayout(self.defaults_host) for row, spec in enumerate(self._default_fields): editor = make_concrete_editor(spec, self.defaults_host) note = QLabel("", self.defaults_host) note.setWordWrap(True) grid.addWidget(QLabel(spec.label, self.defaults_host), row, 0) grid.addWidget(editor, row, 1) grid.addWidget(note, row, 2) self._default_editors[spec.key] = editor self._default_notes[spec.key] = note grid.setColumnStretch(1, 1) grid.setColumnStretch(2, 2) grid.setRowStretch(len(self._default_fields), 1)
[docs] def initGui(self) -> None: # noqa: N802 """Set up UI elements.""" # header report_context_message: str = quote( "> Reported from plugin settings\n\n" f"- operating system: {platform.system()} {platform.release()}_{platform.version()}\n" f"- QGIS: {Qgis.version()}\n" f"- plugin version: {__version__}\n" ) # header self.lbl_title.setText(f"{__title__} - Version {__version__}") # customization self.btn_help.setIcon(QIcon(QgsApplication.iconPath("mActionHelpContents.svg"))) self.btn_help.pressed.connect(partial(QDesktopServices.openUrl, QUrl(__uri_homepage__))) self.btn_report.setIcon( QIcon(QgsApplication.iconPath("console/iconSyntaxErrorConsole.svg")) ) self.btn_report.pressed.connect( partial( QDesktopServices.openUrl, QUrl( f"{__uri_tracker__}new/?" "template=10_bug_report.yml" f"&about-info={report_context_message}" ), ) ) self.btn_reset.setIcon(QIcon(QgsApplication.iconPath("mActionUndo.svg"))) self.btn_reset.pressed.connect(self.on_reset_settings) # load previously saved settings self.load_settings()
[docs] @override def apply(self) -> None: """ Permanently apply settings shown in options page (e.g. save them to QgsSettings objects). This is usually called when the options dialog is accepted. A rejected write is contained and reported here, as this is a QGIS-invoked boundary and the failure must not escape into the options dialog. """ try: # misc self.plg_settings.debug_mode = self.opt_debug.isChecked() self.plg_settings.version_saved = __version__ # algorithm defaults (the ✓ rows of SPEC §3) for spec in self._default_fields: token = concrete_value(spec, self._default_editors[spec.key]) setattr(self.plg_settings, spec.key, self._coerce_for_setting(spec, token)) except RuntimeError: log.exception( self.tr("Could not save plugin settings."), targets=Target.LOG | Target.BAR ) return # the project-variable overrides may now differ; refresh the notes. self._refresh_override_notes() # keep the live root logger level in sync with the just-saved debug_mode. QgisLoggerWrapper.get_logger(__name__.partition(".")[0]).logger.setLevel( logging.DEBUG if self.opt_debug.isChecked() else logging.INFO ) log.debug("DEBUG - Settings successfully saved.")
[docs] @staticmethod def _coerce_for_setting(spec: FieldSpec, raw: str) -> bool | int | str: """ Coerce an editor's string token to the type its setting descriptor stores. :param spec: The field being written. :param raw: The editor's value as a string token. :return: The value typed for the setting (bool / int / str). """ if spec.kind is FieldKind.BOOL: return raw == "true" if spec.kind is FieldKind.INT: return int(raw or 0) return raw
[docs] def load_settings(self) -> None: """Load options from QgsSettings into UI form.""" # global self.opt_debug.setChecked(self.plg_settings.debug_mode) self.lbl_version_saved_value.setText(self.plg_settings.version_saved) # algorithm defaults for spec in self._default_fields: set_concrete_value( spec, self._default_editors[spec.key], str(getattr(self.plg_settings, spec.key)) ) self._refresh_override_notes()
[docs] def _refresh_override_notes(self) -> None: """Show, per field, whether the active project shadows the setting (SPEC §19).""" project_variables = ProjectVariables(project=QgsProject.instance()) for spec in self._default_fields: raw = project_variables.get(spec.variable) note = self._default_notes[spec.key] if raw is not None and str(raw) != "": note.setText(self.tr("⚠️ overridden by project variable (= {})").format(raw)) else: note.setText("")
[docs] def on_reset_settings(self) -> None: """Reset settings to default values (set in the settings module).""" self.plg_settings.reset_defaults() # update the form self.load_settings()
[docs] class PluginOptionsPageWidgetFactory(QgsOptionsWidgetFactory): """Factory for options widget."""
[docs] @override def icon(self) -> QIcon: """ Return plugin icon, used as tab icon in QGIS options tab widget. :return: plugin's icon """ return plugin_icon()
[docs] @override def createWidget(self, parent: QWidget | None = None) -> PluginOptionsPageWidget | None: """ Create settings widget. :param parent: Qt parent where to include the options page. :return: options page for tab widget """ return PluginOptionsPageWidget(parent) if parent else None
[docs] @override def title(self) -> str: """ Return plugin title, used to name the tab in QGIS options tab widget. :return: plugin title from about module """ return __title__