"""Per-layer properties page editing the §4 layer variables (SPEC §4/§19)."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, override
from qgis.core import Qgis, QgsVectorLayer
from qgis.gui import QgsMapLayerConfigWidget, QgsMapLayerConfigWidgetFactory
from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import QFormLayout
from stratified_packager.__about__ import __title__
from stratified_packager.identity import plugin_icon
from stratified_packager.processing.params import LAYER_VAR_MATERIALIZE_VIRTUAL, VARIABLE_PREFIX
from stratified_packager.toolbelt.logging import QgisLoggerWrapper, Target
from stratified_packager.toolbelt.settings import LayerVariables
from .widgets import FieldSpec, OverrideForm, apply_overrides, layer_fields
if TYPE_CHECKING:
from qgis.core import QgsMapLayer
from qgis.gui import QgsMapCanvas
from qgis.PyQt.QtWidgets import QLabel, 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 LayerOptionsPageWidget(QgsMapLayerConfigWidget, FORM_CLASS): # type: ignore[misc,valid-type]
"""Layer Properties page editing one layer's ``stratified_packager_*`` variables."""
verticalLayout: QVBoxLayout # noqa: N815
lbl_header: QLabel
form_host: QWidget
[docs]
@override
def __init__(
self,
layer: QgsMapLayer,
canvas: QgsMapCanvas | None = None,
parent: QWidget | None = None,
) -> None:
"""
Build the page and load *layer*'s variables.
:param layer: The layer being configured.
:param canvas: The map canvas (unused; kept for the framework signature).
:param parent: The host widget supplied by the Layer Properties dialog.
"""
super().__init__(layer, canvas, parent)
self.setupUi(self)
self._layer = layer
self._fields: tuple[FieldSpec, ...] = layer_fields()
self.variables_form: OverrideForm = OverrideForm(self._fields, inheriting=False)
field_names = layer.fields().names() if isinstance(layer, QgsVectorLayer) else []
self.variables_form.build(QFormLayout(self.form_host), self.form_host, field_names)
if layer.providerType() != "virtual":
self.variables_form.set_enabled(
LAYER_VAR_MATERIALIZE_VIRTUAL.removeprefix(VARIABLE_PREFIX), enabled=False
)
self._load()
[docs]
def _load(self) -> None:
"""Populate the editors from the explicit layer variables (unset = builtin default)."""
variables = LayerVariables(self._layer)
current: dict[str, str | None] = {}
for spec in self._fields:
raw = variables.get(spec.variable)
current[spec.key] = None if raw is None or str(raw) == "" else str(raw)
self.variables_form.load(current, {})
[docs]
@override
def apply(self) -> None:
"""
Write the explicit layer variables, clearing inherited fields.
Contained at this QGIS-invoked boundary: a failed write is reported, not raised.
"""
try:
apply_overrides(LayerVariables(self._layer), self.variables_form.dump_variables())
except RuntimeError:
log.exception(
self.tr("Could not save the layer variables."),
targets=Target.LOG | Target.BAR,
)
[docs]
class LayerOptionsPageWidgetFactory(QgsMapLayerConfigWidgetFactory):
"""Factory registering :class:`LayerOptionsPageWidget` in the Layer Properties dialog."""
[docs]
@override
def __init__(self) -> None:
"""Initialize the factory: titled, plugin-iconed, properties-dialog only."""
super().__init__(__title__, plugin_icon())
self.setSupportLayerPropertiesDialog(True)
self.setSupportsStyleDock(False)
[docs]
@override
def supportsLayer(self, layer: QgsMapLayer | None) -> bool:
"""
Report whether *layer* gets the page (vector layers only — §4 is vector-scoped).
:param layer: The candidate layer.
:return: :data:`True` for vector layers.
"""
return layer is not None and layer.type() == Qgis.LayerType.Vector
[docs]
@override
def supportLayerPropertiesDialog(self) -> bool:
"""
Flag if widget is supported for use in layer properties dialog.
:return: :data:`True`
"""
return True
[docs]
@override
def createWidget(
self,
layer: QgsMapLayer | None,
canvas: QgsMapCanvas | None,
dockWidget: bool = True, # framework signature
parent: QWidget | None = None,
) -> LayerOptionsPageWidget:
"""
Create the per-layer variables page.
:param layer: The layer being configured (the framework only calls this when
:meth:`supportsLayer` returned :data:`True`, so it is never :data:`None`).
:param canvas: The map canvas (unused by the page).
:param dockWidget: Whether hosted in a dock (unused; framework signature).
:param parent: The dialog-supplied parent widget.
:return: The page.
:raise ValueError: If called without a layer (a framework-contract violation).
"""
if layer is None:
msg = "createWidget requires a layer."
raise ValueError(msg)
return LayerOptionsPageWidget(layer, canvas, parent)