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:
2026-05-22 09:11:05 -04:00
co-authored by Claude Sonnet 4.6
parent 6cff9557b3
commit 06b951daf7
11 changed files with 857 additions and 76 deletions
+15 -6
View File
@@ -22,6 +22,7 @@ from PyQt6.QtGui import QColor, QBrush, QFont, QKeySequence, QShortcut
from app.utils.worker import TableDataWorker
from app.config.settings import get_settings
from app.config.theme import get_palette
# ─────────────────────────────────────────────────────────────────────────────
@@ -148,18 +149,20 @@ class EditableTableModel(QAbstractTableModel):
return "" if val is None else str(val)
if role == Qt.ItemDataRole.ForegroundRole:
p = get_palette()
if self._is_deleted_row(r):
return QBrush(QColor("#e78284")) # red — pending delete
return QBrush(QColor(p["delete_fg"]))
if (r, c) in self._dirty:
return QBrush(QColor("#ef9f76")) # orange — edited
return QBrush(QColor(p["dirty_fg"]))
if val is None:
return QBrush(QColor("#737994")) # grey — NULL
return QBrush(QColor(p["overlay"]))
if role == Qt.ItemDataRole.BackgroundRole:
p = get_palette()
if self._is_deleted_row(r):
return QBrush(QColor("#3a2530"))
return QBrush(QColor(p["delete_bg"]))
if (r, c) in self._dirty:
return QBrush(QColor("#3a2f25"))
return QBrush(QColor(p["dirty_bg"]))
return None
@@ -444,7 +447,7 @@ class TableViewer(QWidget):
self._frozen_view.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Fixed)
self._frozen_view.setStyleSheet(
"QTableView { border: none; border-right: 2px solid #8caaee; }")
f"QTableView {{ border: none; border-right: 2px solid {get_palette()['blue']}; }}")
self._frozen_view.hide()
# Sync vertical scroll between the two views
@@ -887,6 +890,12 @@ class TableViewer(QWidget):
)
QApplication.clipboard().setText("\n".join(statements))
def apply_settings(self):
"""Called by MainWindow when preferences (including theme) change."""
self._frozen_view.setStyleSheet(
f"QTableView {{ border: none; border-right: 2px solid {get_palette()['blue']}; }}")
self._model.layoutChanged.emit() # repaint dirty/delete cells
def _filter_by_cell(self, index):
if not index.isValid():
return