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:
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Theme palettes — Catppuccin Frappé (dark) and Catppuccin Latte (light).
|
||||
|
||||
Usage:
|
||||
from app.config.theme import get_palette, apply_qss, is_dark
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ── Catppuccin Frappé ─────────────────────────────────────────────────────────
|
||||
DARK = {
|
||||
"base": "#303446",
|
||||
"mantle": "#292c3c",
|
||||
"crust": "#232634",
|
||||
"surface0": "#414559",
|
||||
"surface1": "#51576d",
|
||||
"surface2": "#626880",
|
||||
"overlay": "#737994",
|
||||
"subtext": "#a5adce",
|
||||
"text": "#c6d0f5",
|
||||
"blue": "#8caaee",
|
||||
"lavender": "#babbf1",
|
||||
"green": "#a6d189",
|
||||
"teal": "#81c8be",
|
||||
"sky": "#99d1db",
|
||||
"red": "#e78284",
|
||||
"peach": "#ef9f76",
|
||||
"yellow": "#e5c890",
|
||||
"mauve": "#ca9ee6",
|
||||
# derived UI tokens
|
||||
"gutter_bg": "#2a2d3e",
|
||||
"dirty_bg": "#3a2f25",
|
||||
"dirty_fg": "#ef9f76",
|
||||
"delete_bg": "#3a2530",
|
||||
"delete_fg": "#e78284",
|
||||
# syntax
|
||||
"syn_keyword": "#8caaee",
|
||||
"syn_type": "#ef9f76",
|
||||
"syn_function": "#a6d189",
|
||||
"syn_string": "#a6d189",
|
||||
"syn_number": "#ef9f76",
|
||||
"syn_comment": "#737994",
|
||||
"syn_operator": "#ca9ee6",
|
||||
"syn_ident": "#99d1db",
|
||||
# completer popup
|
||||
"popup_bg": "#292c3c",
|
||||
"popup_border": "#51576d",
|
||||
"popup_selected": "#414559",
|
||||
"popup_hover": "#363a4f",
|
||||
}
|
||||
|
||||
# ── Catppuccin Latte ──────────────────────────────────────────────────────────
|
||||
LIGHT = {
|
||||
"base": "#eff1f5",
|
||||
"mantle": "#e6e9ef",
|
||||
"crust": "#dce0e8",
|
||||
"surface0": "#ccd0da",
|
||||
"surface1": "#bcc0cc",
|
||||
"surface2": "#acb0be",
|
||||
"overlay": "#9ca0b0",
|
||||
"subtext": "#6c6f85",
|
||||
"text": "#4c4f69",
|
||||
"blue": "#1e66f5",
|
||||
"lavender": "#7287fd",
|
||||
"green": "#40a02b",
|
||||
"teal": "#179299",
|
||||
"sky": "#04a5e5",
|
||||
"red": "#d20f39",
|
||||
"peach": "#fe640b",
|
||||
"yellow": "#df8e1d",
|
||||
"mauve": "#8839ef",
|
||||
# derived UI tokens
|
||||
"gutter_bg": "#dce0e8",
|
||||
"dirty_bg": "#fff3e6",
|
||||
"dirty_fg": "#fe640b",
|
||||
"delete_bg": "#ffe8ed",
|
||||
"delete_fg": "#d20f39",
|
||||
# syntax
|
||||
"syn_keyword": "#1e66f5",
|
||||
"syn_type": "#fe640b",
|
||||
"syn_function": "#40a02b",
|
||||
"syn_string": "#40a02b",
|
||||
"syn_number": "#fe640b",
|
||||
"syn_comment": "#9ca0b0",
|
||||
"syn_operator": "#8839ef",
|
||||
"syn_ident": "#04a5e5",
|
||||
# completer popup
|
||||
"popup_bg": "#e6e9ef",
|
||||
"popup_border": "#bcc0cc",
|
||||
"popup_selected": "#ccd0da",
|
||||
"popup_hover": "#d8dce8",
|
||||
}
|
||||
|
||||
_QSS_DIR = Path(__file__).parent.parent.parent / "resources"
|
||||
|
||||
|
||||
def get_palette() -> dict:
|
||||
"""Return the color dict for the currently active theme."""
|
||||
from app.config.settings import get_settings
|
||||
name = get_settings().get("theme", "dark")
|
||||
return DARK if name == "dark" else LIGHT
|
||||
|
||||
|
||||
def is_dark() -> bool:
|
||||
from app.config.settings import get_settings
|
||||
return get_settings().get("theme", "dark") == "dark"
|
||||
|
||||
|
||||
def qss_path(theme: str) -> Path:
|
||||
name = "style" if theme == "dark" else "style_light"
|
||||
return _QSS_DIR / f"{name}.qss"
|
||||
|
||||
|
||||
def apply_qss(app, theme: str = None) -> None:
|
||||
"""Load and apply the QSS for *theme* (defaults to current setting)."""
|
||||
from app.config.settings import get_settings
|
||||
if theme is None:
|
||||
theme = get_settings().get("theme", "dark")
|
||||
path = qss_path(theme)
|
||||
if path.exists():
|
||||
app.setStyleSheet(path.read_text(encoding="utf-8"))
|
||||
+28
-1
@@ -17,6 +17,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QAction, QColor, QKeySequence
|
||||
from app.config.theme import apply_qss, is_dark
|
||||
|
||||
from app.ui.schema_browser import SchemaBrowser
|
||||
from app.ui.sql_editor import SQLEditorWidget
|
||||
@@ -208,6 +209,12 @@ class MainWindow(QMainWindow):
|
||||
view_menu.addAction(self._act("Toggle Query History", self._toggle_history, "Ctrl+Shift+H"))
|
||||
view_menu.addAction(self._act("New SQL Tab", self._new_sql_tab, "Ctrl+T"))
|
||||
view_menu.addAction(self._act("Close Tab", self._close_current_tab, "Ctrl+W"))
|
||||
view_menu.addSeparator()
|
||||
self._theme_action = QAction("", self)
|
||||
self._theme_action.setShortcut(QKeySequence("Ctrl+Shift+T"))
|
||||
self._theme_action.triggered.connect(self._toggle_theme)
|
||||
self._update_theme_action_label()
|
||||
view_menu.addAction(self._theme_action)
|
||||
|
||||
# ── Tools ─────────────────────────────────────────────────────────────
|
||||
tools_menu = mb.addMenu("&Tools")
|
||||
@@ -662,9 +669,29 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _open_preferences(self):
|
||||
dlg = PreferencesDialog(parent=self)
|
||||
dlg.settings_applied.connect(self._apply_settings_to_open_tabs)
|
||||
dlg.settings_applied.connect(self._on_settings_applied)
|
||||
dlg.exec()
|
||||
|
||||
def _on_settings_applied(self):
|
||||
apply_qss(QApplication.instance())
|
||||
self._update_theme_action_label()
|
||||
self._apply_settings_to_open_tabs()
|
||||
|
||||
def _toggle_theme(self):
|
||||
from app.config.settings import get_settings
|
||||
new_theme = "light" if is_dark() else "dark"
|
||||
get_settings().set("theme", new_theme)
|
||||
get_settings().save()
|
||||
apply_qss(QApplication.instance(), new_theme)
|
||||
self._update_theme_action_label()
|
||||
self._apply_settings_to_open_tabs()
|
||||
|
||||
def _update_theme_action_label(self):
|
||||
if is_dark():
|
||||
self._theme_action.setText("☀️ Switch to Light Theme")
|
||||
else:
|
||||
self._theme_action.setText("🌙 Switch to Dark Theme")
|
||||
|
||||
def _apply_settings_to_open_tabs(self):
|
||||
"""Push updated settings to all currently open workspace tabs."""
|
||||
for i in range(self._workspace.count()):
|
||||
|
||||
@@ -7,11 +7,15 @@ from PyQt6.QtCore import (
|
||||
)
|
||||
from PyQt6.QtGui import QColor, QFont
|
||||
|
||||
from app.config.theme import get_palette
|
||||
|
||||
|
||||
class ResultTableModel(QAbstractTableModel):
|
||||
"""Immutable result-set model — replaces data via set_data()."""
|
||||
|
||||
NULL_COLOR = QColor("#737994") # muted grey for NULL
|
||||
@property
|
||||
def NULL_COLOR(self):
|
||||
return QColor(get_palette()["overlay"])
|
||||
NUM_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
TEXT_ALIGN = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ 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):
|
||||
@@ -64,6 +66,18 @@ class PreferencesDialog(QDialog):
|
||||
|
||||
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)
|
||||
@@ -82,7 +96,7 @@ class PreferencesDialog(QDialog):
|
||||
root.addStretch()
|
||||
|
||||
# ── Buttons ───────────────────────────────────────────────────────────
|
||||
note = QLabel("Font changes take effect immediately in open editors.")
|
||||
note = QLabel("Theme and font changes take effect immediately.")
|
||||
note.setObjectName("statusLabel")
|
||||
root.addWidget(note)
|
||||
|
||||
@@ -116,6 +130,10 @@ class PreferencesDialog(QDialog):
|
||||
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())
|
||||
@@ -125,6 +143,7 @@ class PreferencesDialog(QDialog):
|
||||
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()
|
||||
|
||||
|
||||
+23
-14
@@ -12,6 +12,7 @@ from PyQt6.QtWidgets import QApplication, QListWidget, QListWidgetItem
|
||||
|
||||
from app.ui.syntax_highlighter import _KEYWORDS, _TYPES, _FUNCTIONS
|
||||
from app.utils.worker import SchemaWorker
|
||||
from app.config.theme import get_palette
|
||||
|
||||
# Combined, deduplicated keyword list used as the static completion pool.
|
||||
SQL_KEYWORDS: list[str] = list(dict.fromkeys(_KEYWORDS + _TYPES + _FUNCTIONS))
|
||||
@@ -51,24 +52,28 @@ class _CompletionPopup(QListWidget):
|
||||
self.setFont(font)
|
||||
self._row_h = QFontMetrics(font).height() + 6
|
||||
|
||||
self.setStyleSheet("""
|
||||
QListWidget {
|
||||
background: #292c3c;
|
||||
color: #c6d0f5;
|
||||
border: 1px solid #51576d;
|
||||
self._apply_popup_style()
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
def _apply_popup_style(self):
|
||||
p = get_palette()
|
||||
self.setStyleSheet(f"""
|
||||
QListWidget {{
|
||||
background: {p['popup_bg']};
|
||||
color: {p['text']};
|
||||
border: 1px solid {p['popup_border']};
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
outline: none;
|
||||
}
|
||||
QListWidget::item { padding: 2px 8px; }
|
||||
QListWidget::item:selected {
|
||||
background: #414559;
|
||||
color: #c6d0f5;
|
||||
}
|
||||
QListWidget::item:hover { background: #363a4f; }
|
||||
}}
|
||||
QListWidget::item {{ padding: 2px 8px; }}
|
||||
QListWidget::item:selected {{
|
||||
background: {p['popup_selected']};
|
||||
color: {p['text']};
|
||||
}}
|
||||
QListWidget::item:hover {{ background: {p['popup_hover']}; }}
|
||||
""")
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
self.itemClicked.connect(
|
||||
lambda item: self._editor._completer._accept_completion(item.text())
|
||||
@@ -142,6 +147,10 @@ class SqlCompleter(QObject):
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def update_theme(self) -> None:
|
||||
"""Reapply popup colors after a theme change."""
|
||||
self._popup._apply_popup_style()
|
||||
|
||||
def set_context(self, driver, database: str) -> None:
|
||||
"""Update the active driver/database and refresh schema cache."""
|
||||
self._driver = driver
|
||||
|
||||
@@ -471,7 +471,7 @@ class EditorTab(QWidget):
|
||||
self._splitter.setHandleWidth(3)
|
||||
|
||||
self._editor = CodeEditor()
|
||||
SQLHighlighter(self._editor.document())
|
||||
self._highlighter = SQLHighlighter(self._editor.document())
|
||||
self._completer = SqlCompleter(self._editor, parent=self)
|
||||
self._editor.set_completer(self._completer)
|
||||
if self._driver:
|
||||
@@ -603,6 +603,8 @@ class EditorTab(QWidget):
|
||||
|
||||
def apply_settings(self) -> None:
|
||||
self._editor.apply_settings()
|
||||
self._highlighter.update_theme()
|
||||
self._completer.update_theme()
|
||||
|
||||
|
||||
# ── Tabbed SQL editor container ───────────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
+15
-6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user