Source code for spacr.qt.widgets.setup_dialog

"""Present optional application preferences on the first spaCR launch.

Questions are grouped by purpose and initialized with functional defaults, so
the dialog can be dismissed without additional configuration. The blurred
background is decorative; all controls remain available when the platform
cannot render it.
"""
from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional

from PySide6.QtCore import QPoint, Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog,
                               QDialogButtonBox, QFormLayout, QGridLayout,
                               QGraphicsBlurEffect, QGraphicsScene,
                               QGraphicsPixmapItem, QGraphicsView, QLabel,
                               QVBoxLayout, QWidget)

LOG = logging.getLogger("spacr.qt.setup_dialog")

#: Ordered groups used to organize first-run preference questions.
GROUPS: List[tuple] = [
    ("How it looks", ("language", "theme", "colour_blind")),
    ("How it runs", ("spacr_mode", "hash_inputs")),
    ("The assistant", ("ai_provider", "ai_default")),
    ("When something breaks", ("issue_prompt", "share_logs")),
]

#: How far the home screen behind is blurred.
BLUR = 18.0


[docs] class SetupDialog(QDialog): """Collect optional first-run preferences.""" def __init__(self, parent: Optional[QWidget] = None): """Build the first-run setup dialog. :param parent: parent widget, or ``None``; also where the backdrop behind the card is taken from. """ super().__init__(parent) self.setWindowTitle("Set spaCR up") self.setModal(True) self._editors: Dict[str, QWidget] = {} outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) self._backdrop_view = self._backdrop(parent) if self._backdrop_view is not None: outer.addWidget(self._backdrop_view) from .setup_card import SetupCard self.card = SetupCard(self) card_layout = QVBoxLayout(self.card) card_layout.setContentsMargins(24, 24, 24, 24) card_layout.setSpacing(14) title = QLabel("<b>Set spaCR up</b>") card_layout.addWidget(title) blurb = QLabel( "Every one of these has a working default, so you can close this " "and change any of them later in Preferences.") blurb.setWordWrap(True) blurb.setObjectName("Muted") card_layout.addWidget(blurb) self._build_groups(card_layout) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.button(QDialogButtonBox.Cancel).setText("Not now") buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) card_layout.addWidget(buttons) if self._backdrop_view is None: outer.addWidget(self.card) else: self.card.setParent(self) self.card.raise_() self.resize(680, 560) def _build_groups(self, layout) -> None: """One form per group, from the model's own question list.""" from ..setup_screen import current, questions asked = {q[0]: q for q in questions()} answers = current() for heading, keys in GROUPS: here = [asked[k] for k in keys if k in asked] if not here: continue label = QLabel(f"<b>{heading}</b>") layout.addWidget(label) form = QFormLayout() form.setContentsMargins(12, 0, 0, 8) for key, caption, _get, _set, choices in here: editor = self._editor(key, choices, answers.get(key)) self._editors[key] = editor form.addRow(caption, editor) layout.addLayout(form) @staticmethod def _editor(key: str, choices, value): """A combo for a choice, a checkbox for a flag.""" if choices: box = QComboBox() for data, caption in choices: box.addItem(str(caption), data) index = box.findData(value) box.setCurrentIndex(index if index >= 0 else 0) return box box = QCheckBox() box.setChecked(bool(value)) return box
[docs] def answers(self) -> Dict[str, Any]: """Return the current value of every displayed preference control.""" out: Dict[str, Any] = {} for key, editor in self._editors.items(): if isinstance(editor, QComboBox): out[key] = editor.currentData() else: out[key] = bool(editor.isChecked()) return out
[docs] def accept(self) -> None: """Apply displayed preferences and record setup completion. Each preference is applied independently. Rejected values are logged without discarding other valid selections. """ from ..setup_screen import apply, current_version, mark_answered trouble = apply(self.answers()) if trouble: LOG.warning("some setup answers were refused: %s", "; ".join(trouble)) mark_answered(current_version()) super().accept()
[docs] def reject(self) -> None: """Dismiss the dialog while retaining defaults and mark setup complete.""" from ..setup_screen import current_version, mark_answered mark_answered(current_version()) super().reject()
def _backdrop(self, parent) -> Optional[QWidget]: """A blurred still of what is behind, or ``None``. CACHED AS A PIXMAP, NOT A LIVE BLUR. The corner accent is a pointer-position readout and has to keep up with the mouse; blurring a live widget underneath would repaint the whole backdrop on every move. This is grabbed once. NONE IS A FINE ANSWER (INVARIANTS 10). Decoration must never be load-bearing: with no parent, or on a platform where the grab or the blur fails, the dialog is a plain dialog with the same controls and the same answers. """ if parent is None: return None try: shot = parent.grab() if shot.isNull(): return None scene = QGraphicsScene(self) item = QGraphicsPixmapItem(QPixmap(shot)) blur = QGraphicsBlurEffect() blur.setBlurRadius(BLUR) item.setGraphicsEffect(blur) scene.addItem(item) view = QGraphicsView(scene, self) view.setFrameShape(QGraphicsView.NoFrame) view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) view.setAttribute(Qt.WA_TransparentForMouseEvents, True) return view except Exception: # noqa: BLE001 LOG.debug("no blurred backdrop on this platform", exc_info=True) return None
[docs] def resizeEvent(self, event): # noqa: N802 - Qt naming """Re-lay the dialog for the new size. :param event: the Qt resize event. """ super().resizeEvent(event) if self._backdrop_view is not None: self._backdrop_view.setGeometry(self.rect()) margin = 48 self.card.setGeometry(self.rect().adjusted( margin, margin, -margin, -margin)) self.card.raise_()
[docs] def open_setup_if_needed(parent=None) -> Optional[SetupDialog]: """Open the first-run setup dialog when required. Parameters ---------- parent : QWidget, optional Parent application window. Returns ------- SetupDialog or None Executed dialog, or ``None`` when setup has already been completed for the current version. """ from ..setup_screen import should_open if not should_open(): return None dialog = SetupDialog(parent) dialog.exec() return dialog