- app/config/theme.py: palette definitions for both themes + apply_qss() helper used by both main.py (startup) and MainWindow (live toggle) - resources/style_light.qss: complete Catppuccin Latte QSS - View menu: ☀️/🌙 Switch Theme action (Ctrl+Shift+T) toggles live - Preferences → Appearance: Color theme dropdown persists choice - SQLHighlighter: _build_rules() reads get_palette(); update_theme() rebuilds rules and rehighlights all open editors on switch - SqlCompleter: popup stylesheet reads get_palette() via _apply_popup_style(); update_theme() called on switch - EditableTableModel: dirty/delete cell colors read get_palette() live - ResultTableModel: NULL_COLOR property reads get_palette() live - TableViewer: apply_settings() refreshes frozen-view border + repaints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
5.9 KiB
Python
153 lines
5.9 KiB
Python
"""Application Preferences dialog."""
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QFormLayout, QGroupBox,
|
|
QComboBox, QSpinBox, QCheckBox, QDialogButtonBox, QLabel,
|
|
)
|
|
from PyQt6.QtCore import pyqtSignal
|
|
|
|
from app.config.settings import get_settings
|
|
|
|
_FONT_FAMILIES = ["Consolas", "JetBrains Mono", "Fira Code", "Cascadia Code", "Courier New"]
|
|
_PAGE_SIZES = [50, 100, 500, 1000, 0] # 0 → All
|
|
_PAGE_LABELS = ["50", "100", "500", "1 000", "All"]
|
|
_THEMES = [("dark", "🌙 Dark (Catppuccin Frappé)"),
|
|
("light", "☀️ Light (Catppuccin Latte)")]
|
|
|
|
|
|
class PreferencesDialog(QDialog):
|
|
settings_applied = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Preferences")
|
|
self.setMinimumWidth(400)
|
|
self.resize(420, 380)
|
|
self._build_ui()
|
|
self._load()
|
|
|
|
def _build_ui(self):
|
|
root = QVBoxLayout(self)
|
|
root.setSpacing(12)
|
|
root.setContentsMargins(16, 16, 16, 16)
|
|
|
|
# ── SQL Editor ────────────────────────────────────────────────────────
|
|
editor_box = QGroupBox("SQL Editor")
|
|
ef = QFormLayout(editor_box)
|
|
ef.setSpacing(8)
|
|
|
|
self._font_family = QComboBox()
|
|
self._font_family.addItems(_FONT_FAMILIES)
|
|
ef.addRow("Font family:", self._font_family)
|
|
|
|
self._font_size = QSpinBox()
|
|
self._font_size.setRange(8, 28)
|
|
self._font_size.setSuffix(" pt")
|
|
ef.addRow("Font size:", self._font_size)
|
|
|
|
self._word_wrap = QCheckBox("Word wrap")
|
|
ef.addRow("", self._word_wrap)
|
|
|
|
root.addWidget(editor_box)
|
|
|
|
# ── Results ───────────────────────────────────────────────────────────
|
|
results_box = QGroupBox("Results")
|
|
rf = QFormLayout(results_box)
|
|
rf.setSpacing(8)
|
|
|
|
self._page_size = QComboBox()
|
|
for lbl in _PAGE_LABELS:
|
|
self._page_size.addItem(lbl)
|
|
rf.addRow("Default page size:", self._page_size)
|
|
|
|
self._timeout = QSpinBox()
|
|
self._timeout.setRange(5, 600)
|
|
self._timeout.setSuffix(" s")
|
|
rf.addRow("Query timeout:", self._timeout)
|
|
|
|
root.addWidget(results_box)
|
|
|
|
# ── Appearance ────────────────────────────────────────────────────────
|
|
appear_box = QGroupBox("Appearance")
|
|
af = QFormLayout(appear_box)
|
|
af.setSpacing(8)
|
|
|
|
self._theme = QComboBox()
|
|
for _, label in _THEMES:
|
|
self._theme.addItem(label)
|
|
af.addRow("Color theme:", self._theme)
|
|
|
|
root.addWidget(appear_box)
|
|
|
|
# ── General ───────────────────────────────────────────────────────────
|
|
general_box = QGroupBox("General")
|
|
gf = QFormLayout(general_box)
|
|
gf.setSpacing(8)
|
|
|
|
self._max_history = QSpinBox()
|
|
self._max_history.setRange(50, 10_000)
|
|
self._max_history.setSingleStep(100)
|
|
gf.addRow("Max history entries:", self._max_history)
|
|
|
|
self._auto_commit = QCheckBox("Auto-commit queries")
|
|
gf.addRow("", self._auto_commit)
|
|
|
|
root.addWidget(general_box)
|
|
|
|
root.addStretch()
|
|
|
|
# ── Buttons ───────────────────────────────────────────────────────────
|
|
note = QLabel("Theme and font changes take effect immediately.")
|
|
note.setObjectName("statusLabel")
|
|
root.addWidget(note)
|
|
|
|
bb = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Cancel |
|
|
QDialogButtonBox.StandardButton.Apply |
|
|
QDialogButtonBox.StandardButton.Ok
|
|
)
|
|
bb.rejected.connect(self.reject)
|
|
bb.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(self._apply)
|
|
bb.accepted.connect(self._ok)
|
|
root.addWidget(bb)
|
|
|
|
# ── Load / save ───────────────────────────────────────────────────────────
|
|
|
|
def _load(self):
|
|
s = get_settings()
|
|
|
|
family = s.get("font_family", "Consolas")
|
|
idx = self._font_family.findText(family)
|
|
self._font_family.setCurrentIndex(max(idx, 0))
|
|
|
|
self._font_size.setValue(s.get("font_size", 13))
|
|
self._word_wrap.setChecked(s.get("word_wrap", False))
|
|
|
|
page = s.get("result_page_size", 100)
|
|
pi = _PAGE_SIZES.index(page) if page in _PAGE_SIZES else 1
|
|
self._page_size.setCurrentIndex(pi)
|
|
|
|
self._timeout.setValue(s.get("query_timeout", 60))
|
|
self._max_history.setValue(s.get("max_history", 500))
|
|
self._auto_commit.setChecked(s.get("auto_commit", True))
|
|
|
|
theme_name = s.get("theme", "dark")
|
|
ti = next((i for i, (k, _) in enumerate(_THEMES) if k == theme_name), 0)
|
|
self._theme.setCurrentIndex(ti)
|
|
|
|
def _apply(self):
|
|
s = get_settings()
|
|
s.set("font_family", self._font_family.currentText())
|
|
s.set("font_size", self._font_size.value())
|
|
s.set("word_wrap", self._word_wrap.isChecked())
|
|
s.set("result_page_size", _PAGE_SIZES[self._page_size.currentIndex()])
|
|
s.set("query_timeout", self._timeout.value())
|
|
s.set("max_history", self._max_history.value())
|
|
s.set("auto_commit", self._auto_commit.isChecked())
|
|
s.set("theme", _THEMES[self._theme.currentIndex()][0])
|
|
s.save()
|
|
self.settings_applied.emit()
|
|
|
|
def _ok(self):
|
|
self._apply()
|
|
self.accept()
|