feat: dark/light theme toggle (Catppuccin Frappé ↔ Latte)
- 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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
SQL syntax highlighter for QPlainTextEdit.
|
||||
Highlights keywords, types, functions, strings, comments, and numbers.
|
||||
Colors follow the active Catppuccin theme (dark = Frappé, light = Latte).
|
||||
"""
|
||||
import re
|
||||
from PyQt6.QtCore import QRegularExpression, Qt
|
||||
@@ -8,6 +9,8 @@ from PyQt6.QtGui import (
|
||||
QSyntaxHighlighter, QTextCharFormat, QColor, QFont
|
||||
)
|
||||
|
||||
from app.config.theme import get_palette
|
||||
|
||||
|
||||
def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
||||
f = QTextCharFormat()
|
||||
@@ -17,17 +20,7 @@ def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharForma
|
||||
return f
|
||||
|
||||
|
||||
# ── Token categories (Catppuccin Frappé palette) ─────────────────────────────
|
||||
_KEYWORD_FMT = _fmt("#8caaee", bold=True) # blue — DDL/DML/control
|
||||
_TYPE_FMT = _fmt("#ef9f76") # peach — data types
|
||||
_FUNCTION_FMT = _fmt("#a6d189") # green — functions
|
||||
_STRING_FMT = _fmt("#a6d189") # green — string literals
|
||||
_NUMBER_FMT = _fmt("#ef9f76") # orange — numeric literals
|
||||
_COMMENT_FMT = _fmt("#737994", italic=True) # grey — comments
|
||||
_OPERATOR_FMT = _fmt("#ca9ee6") # mauve — operators/special
|
||||
_STAR_FMT = _fmt("#ca9ee6", bold=True) # mauve — * wildcard
|
||||
|
||||
# ── Keyword lists ─────────────────────────────────────────────────────────────
|
||||
# ── Keyword lists (shared with completer) ─────────────────────────────────────
|
||||
_KEYWORDS = [
|
||||
"SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER",
|
||||
"FULL", "CROSS", "ON", "AS", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
|
||||
@@ -79,56 +72,56 @@ class SQLHighlighter(QSyntaxHighlighter):
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
self._rules: list[tuple] = []
|
||||
self._build_rules()
|
||||
|
||||
def _build_rules(self):
|
||||
p = get_palette()
|
||||
|
||||
def kw_pattern(words: list[str]) -> str:
|
||||
return r"\b(?:" + "|".join(words) + r")\b"
|
||||
|
||||
ci = QRegularExpression.PatternOption.CaseInsensitiveOption
|
||||
self._rules = [
|
||||
# Keywords (case-insensitive handled via flag)
|
||||
(QRegularExpression(kw_pattern(_KEYWORDS),
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_KEYWORD_FMT),
|
||||
# Data types
|
||||
(QRegularExpression(kw_pattern(_TYPES),
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_TYPE_FMT),
|
||||
# Functions
|
||||
(QRegularExpression(kw_pattern(_FUNCTIONS),
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_FUNCTION_FMT),
|
||||
# Numbers
|
||||
(QRegularExpression(r"\b\d+(\.\d+)?\b"), _NUMBER_FMT),
|
||||
# Single-quoted strings
|
||||
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"), _STRING_FMT),
|
||||
# Double-quoted identifiers
|
||||
(QRegularExpression(r'"[^"]*"'), _fmt("#99d1db")),
|
||||
# Backtick identifiers (MySQL)
|
||||
(QRegularExpression(r"`[^`]*`"), _fmt("#99d1db")),
|
||||
# * wildcard
|
||||
(QRegularExpression(r"\bSELECT\s+\*|\*(?=\s*FROM)",
|
||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
||||
_STAR_FMT),
|
||||
# Operators
|
||||
(QRegularExpression(r"[=<>!%&|^~+\-*/]"), _OPERATOR_FMT),
|
||||
# Single-line comment --
|
||||
(QRegularExpression(r"--[^\n]*"), _COMMENT_FMT),
|
||||
# Single-line comment #
|
||||
(QRegularExpression(r"#[^\n]*"), _COMMENT_FMT),
|
||||
(QRegularExpression(kw_pattern(_KEYWORDS), ci),
|
||||
_fmt(p["syn_keyword"], bold=True)),
|
||||
(QRegularExpression(kw_pattern(_TYPES), ci),
|
||||
_fmt(p["syn_type"])),
|
||||
(QRegularExpression(kw_pattern(_FUNCTIONS), ci),
|
||||
_fmt(p["syn_function"])),
|
||||
(QRegularExpression(r"\b\d+(\.\d+)?\b"),
|
||||
_fmt(p["syn_number"])),
|
||||
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"),
|
||||
_fmt(p["syn_string"])),
|
||||
(QRegularExpression(r'"[^"]*"'),
|
||||
_fmt(p["syn_ident"])),
|
||||
(QRegularExpression(r"`[^`]*`"),
|
||||
_fmt(p["syn_ident"])),
|
||||
(QRegularExpression(
|
||||
r"\bSELECT\s+\*|\*(?=\s*FROM)", ci),
|
||||
_fmt(p["syn_operator"], bold=True)),
|
||||
(QRegularExpression(r"[=<>!%&|^~+\-*/]"),
|
||||
_fmt(p["syn_operator"])),
|
||||
(QRegularExpression(r"--[^\n]*"),
|
||||
_fmt(p["syn_comment"], italic=True)),
|
||||
(QRegularExpression(r"#[^\n]*"),
|
||||
_fmt(p["syn_comment"], italic=True)),
|
||||
]
|
||||
|
||||
# Multi-line block comment /* ... */
|
||||
self._comment_fmt = _fmt(p["syn_comment"], italic=True)
|
||||
self._block_comment_start = QRegularExpression(r"/\*")
|
||||
self._block_comment_end = QRegularExpression(r"\*/")
|
||||
|
||||
def update_theme(self):
|
||||
"""Rebuild color rules for the current theme and re-highlight."""
|
||||
self._build_rules()
|
||||
self.rehighlight()
|
||||
|
||||
def highlightBlock(self, text: str) -> None:
|
||||
# Single-line rules
|
||||
for pattern, fmt in self._rules:
|
||||
it = pattern.globalMatch(text)
|
||||
while it.hasNext():
|
||||
m = it.next()
|
||||
self.setFormat(m.capturedStart(), m.capturedLength(), fmt)
|
||||
|
||||
# Multi-line block comments
|
||||
self.setCurrentBlockState(0)
|
||||
start_idx = 0
|
||||
if self.previousBlockState() != 1:
|
||||
@@ -139,10 +132,10 @@ class SQLHighlighter(QSyntaxHighlighter):
|
||||
end_m = self._block_comment_end.match(text, start_idx)
|
||||
if end_m.hasMatch():
|
||||
end_idx = end_m.capturedStart() + end_m.capturedLength()
|
||||
self.setFormat(start_idx, end_idx - start_idx, _COMMENT_FMT)
|
||||
self.setFormat(start_idx, end_idx - start_idx, self._comment_fmt)
|
||||
m = self._block_comment_start.match(text, end_idx)
|
||||
start_idx = m.capturedStart() if m.hasMatch() else -1
|
||||
else:
|
||||
self.setCurrentBlockState(1)
|
||||
self.setFormat(start_idx, len(text) - start_idx, _COMMENT_FMT)
|
||||
self.setFormat(start_idx, len(text) - start_idx, self._comment_fmt)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user