feat: add 5 new themes via single QSS template engine
Replaces the two hard-coded .qss files with a single
resources/style_template.qss that uses @{token} placeholders.
apply_qss() now does regex substitution at runtime, so adding a
theme only requires a palette dict — no duplicate stylesheets.
New themes (Preferences → Color theme):
• One Dark Pro — VS Code classic dark
• Nord — arctic blue-gray dark
• Tokyo Night — deep purple-dark
• Dracula — classic purple-dark
• GitHub Light — clean minimal light
Existing Catppuccin Frappé (dark) and Latte (light) are unchanged.
Ctrl+Shift+T quick-toggle now recognises all dark themes via is_dark().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+212
-104
@@ -1,121 +1,229 @@
|
|||||||
"""
|
"""
|
||||||
Theme palettes — Catppuccin Frappé (dark) and Catppuccin Latte (light).
|
Theme palettes and QSS application.
|
||||||
|
|
||||||
Usage:
|
Each theme is a flat dict of color tokens. apply_qss() loads
|
||||||
from app.config.theme import get_palette, apply_qss, is_dark
|
style_template.qss and substitutes @{key} tokens with the palette values.
|
||||||
|
|
||||||
|
Supported themes
|
||||||
|
----------------
|
||||||
|
dark Catppuccin Frappé (dark blue-gray)
|
||||||
|
light Catppuccin Latte (warm light)
|
||||||
|
one_dark One Dark Pro (VS Code classic dark)
|
||||||
|
nord Nord (arctic blue-gray dark)
|
||||||
|
tokyo_night Tokyo Night (purple-dark)
|
||||||
|
dracula Dracula (classic purple-dark)
|
||||||
|
github_light GitHub Light (clean minimal light)
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import re
|
||||||
from pathlib import Path
|
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"
|
_QSS_DIR = Path(__file__).parent.parent.parent / "resources"
|
||||||
|
|
||||||
|
# ── Palettes ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Catppuccin Frappé — dark blue-gray
|
||||||
|
_FRAPPÉ: dict = {
|
||||||
|
"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",
|
||||||
|
"red_hover": "#ea999c",
|
||||||
|
"tab_hover": "#353849", "tree_hover": "#363a4f", "btn_pressed": "#363849",
|
||||||
|
"gutter_bg": "#2a2d3e",
|
||||||
|
"dirty_bg": "#3a2f25", "dirty_fg": "#ef9f76",
|
||||||
|
"delete_bg": "#3a2530", "delete_fg": "#e78284",
|
||||||
|
"syn_keyword": "#8caaee", "syn_type": "#ef9f76",
|
||||||
|
"syn_function": "#a6d189", "syn_string": "#a6d189",
|
||||||
|
"syn_number": "#ef9f76", "syn_comment": "#737994",
|
||||||
|
"syn_operator": "#ca9ee6", "syn_ident": "#99d1db",
|
||||||
|
"popup_bg": "#292c3c", "popup_border": "#51576d",
|
||||||
|
"popup_selected": "#414559", "popup_hover": "#363a4f",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Catppuccin Latte — warm light
|
||||||
|
_LATTE: dict = {
|
||||||
|
"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",
|
||||||
|
"red_hover": "#da2b50",
|
||||||
|
"tab_hover": "#d8dce8", "tree_hover": "#d8dce8", "btn_pressed": "#c8cdd9",
|
||||||
|
"gutter_bg": "#dce0e8",
|
||||||
|
"dirty_bg": "#fff3e6", "dirty_fg": "#fe640b",
|
||||||
|
"delete_bg": "#ffe8ed", "delete_fg": "#d20f39",
|
||||||
|
"syn_keyword": "#1e66f5", "syn_type": "#fe640b",
|
||||||
|
"syn_function": "#40a02b", "syn_string": "#40a02b",
|
||||||
|
"syn_number": "#fe640b", "syn_comment": "#9ca0b0",
|
||||||
|
"syn_operator": "#8839ef", "syn_ident": "#04a5e5",
|
||||||
|
"popup_bg": "#e6e9ef", "popup_border": "#bcc0cc",
|
||||||
|
"popup_selected": "#ccd0da", "popup_hover": "#d8dce8",
|
||||||
|
}
|
||||||
|
|
||||||
|
# One Dark Pro — VS Code classic
|
||||||
|
_ONE_DARK: dict = {
|
||||||
|
"base": "#282c34", "mantle": "#21252b", "crust": "#1d2026",
|
||||||
|
"surface0": "#2c313c", "surface1": "#3e4452", "surface2": "#4b5363",
|
||||||
|
"overlay": "#5c6370", "subtext": "#828997", "text": "#abb2bf",
|
||||||
|
"blue": "#61afef", "lavender": "#c678dd", "green": "#98c379",
|
||||||
|
"teal": "#56b6c2", "sky": "#56b6c2", "red": "#e06c75",
|
||||||
|
"peach": "#d19a66", "yellow": "#e5c07b", "mauve": "#c678dd",
|
||||||
|
"red_hover": "#e8868f",
|
||||||
|
"tab_hover": "#2e3440", "tree_hover": "#2c313c", "btn_pressed": "#282c34",
|
||||||
|
"gutter_bg": "#1e2127",
|
||||||
|
"dirty_bg": "#3a2f1e", "dirty_fg": "#d19a66",
|
||||||
|
"delete_bg": "#3a1e20", "delete_fg": "#e06c75",
|
||||||
|
"syn_keyword": "#c678dd", "syn_type": "#e5c07b",
|
||||||
|
"syn_function": "#61afef", "syn_string": "#98c379",
|
||||||
|
"syn_number": "#d19a66", "syn_comment": "#5c6370",
|
||||||
|
"syn_operator": "#56b6c2", "syn_ident": "#abb2bf",
|
||||||
|
"popup_bg": "#21252b", "popup_border": "#3e4452",
|
||||||
|
"popup_selected": "#2c313c", "popup_hover": "#2e3440",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nord — arctic blue-gray
|
||||||
|
_NORD: dict = {
|
||||||
|
"base": "#2e3440", "mantle": "#292e3b", "crust": "#242831",
|
||||||
|
"surface0": "#3b4252", "surface1": "#434c5e", "surface2": "#4c566a",
|
||||||
|
"overlay": "#616e88", "subtext": "#d8dee9", "text": "#eceff4",
|
||||||
|
"blue": "#88c0d0", "lavender": "#b48ead", "green": "#a3be8c",
|
||||||
|
"teal": "#8fbcbb", "sky": "#81a1c1", "red": "#bf616a",
|
||||||
|
"peach": "#d08770", "yellow": "#ebcb8b", "mauve": "#b48ead",
|
||||||
|
"red_hover": "#ca737b",
|
||||||
|
"tab_hover": "#333a47", "tree_hover": "#323844", "btn_pressed": "#2e3440",
|
||||||
|
"gutter_bg": "#272c38",
|
||||||
|
"dirty_bg": "#3a3320", "dirty_fg": "#d08770",
|
||||||
|
"delete_bg": "#3a2730", "delete_fg": "#bf616a",
|
||||||
|
"syn_keyword": "#81a1c1", "syn_type": "#ebcb8b",
|
||||||
|
"syn_function": "#88c0d0", "syn_string": "#a3be8c",
|
||||||
|
"syn_number": "#b48ead", "syn_comment": "#616e88",
|
||||||
|
"syn_operator": "#8fbcbb", "syn_ident": "#eceff4",
|
||||||
|
"popup_bg": "#292e3b", "popup_border": "#434c5e",
|
||||||
|
"popup_selected": "#3b4252", "popup_hover": "#323844",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tokyo Night — deep purple-dark
|
||||||
|
_TOKYO_NIGHT: dict = {
|
||||||
|
"base": "#1a1b2e", "mantle": "#16161e", "crust": "#13131a",
|
||||||
|
"surface0": "#24283b", "surface1": "#292e42", "surface2": "#2f3549",
|
||||||
|
"overlay": "#565f89", "subtext": "#9aa5ce", "text": "#c0caf5",
|
||||||
|
"blue": "#7aa2f7", "lavender": "#bb9af7", "green": "#9ece6a",
|
||||||
|
"teal": "#73daca", "sky": "#7dcfff", "red": "#f7768e",
|
||||||
|
"peach": "#ff9e64", "yellow": "#e0af68", "mauve": "#bb9af7",
|
||||||
|
"red_hover": "#f98da0",
|
||||||
|
"tab_hover": "#1e2030", "tree_hover": "#1e2030", "btn_pressed": "#1a1b2e",
|
||||||
|
"gutter_bg": "#16161e",
|
||||||
|
"dirty_bg": "#2a2216", "dirty_fg": "#ff9e64",
|
||||||
|
"delete_bg": "#2a1620", "delete_fg": "#f7768e",
|
||||||
|
"syn_keyword": "#bb9af7", "syn_type": "#e0af68",
|
||||||
|
"syn_function": "#7aa2f7", "syn_string": "#9ece6a",
|
||||||
|
"syn_number": "#ff9e64", "syn_comment": "#565f89",
|
||||||
|
"syn_operator": "#73daca", "syn_ident": "#c0caf5",
|
||||||
|
"popup_bg": "#16161e", "popup_border": "#292e42",
|
||||||
|
"popup_selected": "#24283b", "popup_hover": "#1e2030",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dracula — classic purple-dark
|
||||||
|
_DRACULA: dict = {
|
||||||
|
"base": "#282a36", "mantle": "#21222c", "crust": "#191a21",
|
||||||
|
"surface0": "#343746", "surface1": "#424450", "surface2": "#54555f",
|
||||||
|
"overlay": "#6272a4", "subtext": "#bfbfbf", "text": "#f8f8f2",
|
||||||
|
"blue": "#8be9fd", "lavender": "#bd93f9", "green": "#50fa7b",
|
||||||
|
"teal": "#8be9fd", "sky": "#8be9fd", "red": "#ff5555",
|
||||||
|
"peach": "#ffb86c", "yellow": "#f1fa8c", "mauve": "#bd93f9",
|
||||||
|
"red_hover": "#ff7373",
|
||||||
|
"tab_hover": "#2d2f3e", "tree_hover": "#2d2f3e", "btn_pressed": "#21222c",
|
||||||
|
"gutter_bg": "#20212b",
|
||||||
|
"dirty_bg": "#3a3020", "dirty_fg": "#ffb86c",
|
||||||
|
"delete_bg": "#3a1e1e", "delete_fg": "#ff5555",
|
||||||
|
"syn_keyword": "#ff79c6", "syn_type": "#ffb86c",
|
||||||
|
"syn_function": "#50fa7b", "syn_string": "#f1fa8c",
|
||||||
|
"syn_number": "#bd93f9", "syn_comment": "#6272a4",
|
||||||
|
"syn_operator": "#ff79c6", "syn_ident": "#8be9fd",
|
||||||
|
"popup_bg": "#21222c", "popup_border": "#424450",
|
||||||
|
"popup_selected": "#343746", "popup_hover": "#2d2f3e",
|
||||||
|
}
|
||||||
|
|
||||||
|
# GitHub Light — clean minimal light
|
||||||
|
_GITHUB_LIGHT: dict = {
|
||||||
|
"base": "#ffffff", "mantle": "#f6f8fa", "crust": "#eaeef2",
|
||||||
|
"surface0": "#f0f3f6", "surface1": "#d0d7de", "surface2": "#afb8c1",
|
||||||
|
"overlay": "#8c959f", "subtext": "#57606a", "text": "#24292f",
|
||||||
|
"blue": "#0969da", "lavender": "#8250df", "green": "#1a7f37",
|
||||||
|
"teal": "#0a7bca", "sky": "#0550ae", "red": "#cf222e",
|
||||||
|
"peach": "#bc4c00", "yellow": "#9a6700", "mauve": "#8250df",
|
||||||
|
"red_hover": "#d93f47",
|
||||||
|
"tab_hover": "#eaeef2", "tree_hover": "#eaeef2", "btn_pressed": "#e0e6ec",
|
||||||
|
"gutter_bg": "#f0f3f6",
|
||||||
|
"dirty_bg": "#fff8e1", "dirty_fg": "#bc4c00",
|
||||||
|
"delete_bg": "#fde8e8", "delete_fg": "#cf222e",
|
||||||
|
"syn_keyword": "#cf222e", "syn_type": "#953800",
|
||||||
|
"syn_function": "#8250df", "syn_string": "#0a3069",
|
||||||
|
"syn_number": "#0550ae", "syn_comment": "#8c959f",
|
||||||
|
"syn_operator": "#cf222e", "syn_ident": "#0969da",
|
||||||
|
"popup_bg": "#f6f8fa", "popup_border": "#d0d7de",
|
||||||
|
"popup_selected": "#e8ecf0", "popup_hover": "#eaeef2",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Registry: theme key → palette dict
|
||||||
|
ALL_THEMES: dict[str, dict] = {
|
||||||
|
"dark": _FRAPPÉ,
|
||||||
|
"light": _LATTE,
|
||||||
|
"one_dark": _ONE_DARK,
|
||||||
|
"nord": _NORD,
|
||||||
|
"tokyo_night": _TOKYO_NIGHT,
|
||||||
|
"dracula": _DRACULA,
|
||||||
|
"github_light": _GITHUB_LIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Which theme keys are considered "dark"
|
||||||
|
_DARK_KEYS = {"dark", "one_dark", "nord", "tokyo_night", "dracula"}
|
||||||
|
|
||||||
|
# Kept for backward compat (imported elsewhere as DARK / LIGHT)
|
||||||
|
DARK = _FRAPPÉ
|
||||||
|
LIGHT = _LATTE
|
||||||
|
|
||||||
|
|
||||||
|
# ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_palette() -> dict:
|
def get_palette() -> dict:
|
||||||
"""Return the color dict for the currently active theme."""
|
"""Return the color dict for the currently active theme."""
|
||||||
from app.config.settings import get_settings
|
from app.config.settings import get_settings
|
||||||
name = get_settings().get("theme", "dark")
|
name = get_settings().get("theme", "dark")
|
||||||
return DARK if name == "dark" else LIGHT
|
return ALL_THEMES.get(name, _FRAPPÉ)
|
||||||
|
|
||||||
|
|
||||||
def is_dark() -> bool:
|
def is_dark(theme: str | None = None) -> 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
|
from app.config.settings import get_settings
|
||||||
if theme is None:
|
if theme is None:
|
||||||
theme = get_settings().get("theme", "dark")
|
theme = get_settings().get("theme", "dark")
|
||||||
path = qss_path(theme)
|
return theme in _DARK_KEYS
|
||||||
if path.exists():
|
|
||||||
app.setStyleSheet(path.read_text(encoding="utf-8"))
|
|
||||||
|
def apply_qss(app, theme: str | None = None) -> None:
|
||||||
|
"""Load the QSS template, substitute palette tokens, and apply to *app*."""
|
||||||
|
from app.config.settings import get_settings
|
||||||
|
if theme is None:
|
||||||
|
theme = get_settings().get("theme", "dark")
|
||||||
|
palette = ALL_THEMES.get(theme, _FRAPPÉ)
|
||||||
|
|
||||||
|
template_path = _QSS_DIR / "style_template.qss"
|
||||||
|
if template_path.exists():
|
||||||
|
template = template_path.read_text(encoding="utf-8")
|
||||||
|
qss = re.sub(r"@\{(\w+)\}", lambda m: palette.get(m.group(1), m.group(0)), template)
|
||||||
|
app.setStyleSheet(qss)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback: legacy per-file approach
|
||||||
|
legacy = _QSS_DIR / ("style.qss" if is_dark(theme) else "style_light.qss")
|
||||||
|
if legacy.exists():
|
||||||
|
app.setStyleSheet(legacy.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def qss_path(theme: str) -> Path:
|
||||||
|
"""Legacy helper — kept so old call-sites don't break."""
|
||||||
|
return _QSS_DIR / ("style.qss" if is_dark(theme) else "style_light.qss")
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ from app.config.settings import get_settings
|
|||||||
_FONT_FAMILIES = ["Consolas", "JetBrains Mono", "Fira Code", "Cascadia Code", "Courier New"]
|
_FONT_FAMILIES = ["Consolas", "JetBrains Mono", "Fira Code", "Cascadia Code", "Courier New"]
|
||||||
_PAGE_SIZES = [50, 100, 500, 1000, 0] # 0 → All
|
_PAGE_SIZES = [50, 100, 500, 1000, 0] # 0 → All
|
||||||
_PAGE_LABELS = ["50", "100", "500", "1 000", "All"]
|
_PAGE_LABELS = ["50", "100", "500", "1 000", "All"]
|
||||||
_THEMES = [("dark", "🌙 Dark (Catppuccin Frappé)"),
|
_THEMES = [
|
||||||
("light", "☀️ Light (Catppuccin Latte)")]
|
("dark", "🌙 Catppuccin Frappé (dark blue-gray)"),
|
||||||
|
("one_dark", "⬛ One Dark Pro (VS Code classic)"),
|
||||||
|
("nord", "❄️ Nord (arctic blue-gray)"),
|
||||||
|
("tokyo_night", "🌃 Tokyo Night (deep purple-dark)"),
|
||||||
|
("dracula", "🧛 Dracula (classic purple-dark)"),
|
||||||
|
("light", "☀️ Catppuccin Latte (warm light)"),
|
||||||
|
("github_light", "📄 GitHub Light (clean minimal)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(QDialog):
|
class PreferencesDialog(QDialog):
|
||||||
@@ -20,8 +27,8 @@ class PreferencesDialog(QDialog):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Preferences")
|
self.setWindowTitle("Preferences")
|
||||||
self.setMinimumWidth(400)
|
self.setMinimumWidth(440)
|
||||||
self.resize(420, 380)
|
self.resize(460, 400)
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._load()
|
self._load()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,589 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
DBClient — Theme template (tokens replaced at runtime by theme.py)
|
||||||
|
═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Global reset ───────────────────────────────────────────────────────── */
|
||||||
|
* {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMainWindow, QDialog {
|
||||||
|
background-color: @{base};
|
||||||
|
color: @{text};
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget {
|
||||||
|
background-color: @{base};
|
||||||
|
color: @{text};
|
||||||
|
font-family: "Segoe UI", "Inter", sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Menu bar ───────────────────────────────────────────────────────────── */
|
||||||
|
QMenuBar {
|
||||||
|
background-color: @{crust};
|
||||||
|
color: @{text};
|
||||||
|
border-bottom: 1px solid @{surface0};
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenuBar::item {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenuBar::item:selected {
|
||||||
|
background-color: @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu {
|
||||||
|
background-color: @{mantle};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu::item {
|
||||||
|
padding: 6px 24px 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: @{text};
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu::item:selected {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu::separator {
|
||||||
|
height: 1px;
|
||||||
|
background: @{surface1};
|
||||||
|
margin: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status bar ─────────────────────────────────────────────────────────── */
|
||||||
|
QStatusBar {
|
||||||
|
background-color: @{crust};
|
||||||
|
border-top: 1px solid @{surface0};
|
||||||
|
color: @{subtext};
|
||||||
|
font-size: 9pt;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scrollbars ─────────────────────────────────────────────────────────── */
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background: @{base};
|
||||||
|
width: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background: @{surface1};
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical:hover {
|
||||||
|
background: @{surface2};
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar:horizontal {
|
||||||
|
background: @{base};
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal {
|
||||||
|
background: @{surface1};
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal:hover {
|
||||||
|
background: @{surface2};
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; }
|
||||||
|
QScrollBar::add-page, QScrollBar::sub-page { background: transparent; }
|
||||||
|
|
||||||
|
/* ── Splitter ────────────────────────────────────────────────────────────── */
|
||||||
|
QSplitter::handle {
|
||||||
|
background-color: @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
QSplitter::handle:horizontal { width: 2px; }
|
||||||
|
QSplitter::handle:vertical { height: 2px; }
|
||||||
|
|
||||||
|
QSplitter::handle:hover {
|
||||||
|
background-color: @{blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar ─────────────────────────────────────────────────────────────── */
|
||||||
|
#sidebarHeader {
|
||||||
|
background-color: @{mantle};
|
||||||
|
border-bottom: 1px solid @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarTitle {
|
||||||
|
color: @{blue};
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#newConnBtn {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{blue};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14pt;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#newConnBtn:hover {
|
||||||
|
background-color: @{surface1};
|
||||||
|
color: @{lavender};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tree widget ─────────────────────────────────────────────────────────── */
|
||||||
|
QTreeWidget {
|
||||||
|
background-color: @{mantle};
|
||||||
|
border: none;
|
||||||
|
color: @{text};
|
||||||
|
font-size: 10pt;
|
||||||
|
show-decoration-selected: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::item {
|
||||||
|
padding: 3px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::item:hover {
|
||||||
|
background-color: @{tree_hover};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::item:selected {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::branch {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab widget ──────────────────────────────────────────────────────────── */
|
||||||
|
QTabWidget::pane {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid @{surface0};
|
||||||
|
background-color: @{base};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar {
|
||||||
|
background-color: @{mantle};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::tab {
|
||||||
|
background-color: @{mantle};
|
||||||
|
color: @{subtext};
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid @{surface0};
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::tab:selected {
|
||||||
|
background-color: @{base};
|
||||||
|
color: @{text};
|
||||||
|
border-bottom: 2px solid @{blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::tab:hover:!selected {
|
||||||
|
background-color: @{tab_hover};
|
||||||
|
color: @{text};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::close-button {
|
||||||
|
subcontrol-position: right;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Table view ──────────────────────────────────────────────────────────── */
|
||||||
|
QTableView, QTableWidget {
|
||||||
|
background-color: @{base};
|
||||||
|
alternate-background-color: @{tree_hover};
|
||||||
|
gridline-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
border: none;
|
||||||
|
selection-background-color: @{surface0};
|
||||||
|
selection-color: @{text};
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTableView::item, QTableWidget::item {
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView {
|
||||||
|
background-color: @{mantle};
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView::section {
|
||||||
|
background-color: @{mantle};
|
||||||
|
color: @{blue};
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid @{surface0};
|
||||||
|
border-bottom: 1px solid @{surface0};
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView::section:hover {
|
||||||
|
background-color: @{tab_hover};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Plain text edit (SQL editor body) ───────────────────────────────────── */
|
||||||
|
QPlainTextEdit {
|
||||||
|
background-color: @{base};
|
||||||
|
color: @{text};
|
||||||
|
border: none;
|
||||||
|
selection-background-color: @{surface1};
|
||||||
|
font-family: "Consolas", "JetBrains Mono", "Courier New", monospace;
|
||||||
|
font-size: 13pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Line number gutter ──────────────────────────────────────────────────── */
|
||||||
|
LineNumberArea {
|
||||||
|
background-color: @{gutter_bg};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Buttons ─────────────────────────────────────────────────────────────── */
|
||||||
|
QPushButton {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 14px;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: @{surface1};
|
||||||
|
border-color: @{surface2};
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: @{btn_pressed};
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:disabled {
|
||||||
|
color: @{surface2};
|
||||||
|
background-color: @{btn_pressed};
|
||||||
|
border-color: @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
#runBtn {
|
||||||
|
background-color: @{green};
|
||||||
|
color: @{base};
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#runBtn:hover {
|
||||||
|
background-color: @{teal};
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopBtn {
|
||||||
|
background-color: @{red};
|
||||||
|
color: @{base};
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopBtn:hover {
|
||||||
|
background-color: @{red_hover};
|
||||||
|
}
|
||||||
|
|
||||||
|
#commitBtn {
|
||||||
|
background-color: @{green};
|
||||||
|
color: @{base};
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rollbackBtn {
|
||||||
|
background-color: @{peach};
|
||||||
|
color: @{base};
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudAddBtn {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{green};
|
||||||
|
border: 1px solid @{green};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudAddBtn:hover {
|
||||||
|
background-color: @{green};
|
||||||
|
color: @{base};
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudDeleteBtn {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{red};
|
||||||
|
border: 1px solid @{red};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudDeleteBtn:hover {
|
||||||
|
background-color: @{red};
|
||||||
|
color: @{base};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Line edit ───────────────────────────────────────────────────────────── */
|
||||||
|
QLineEdit {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 10pt;
|
||||||
|
selection-background-color: @{blue};
|
||||||
|
selection-color: @{base};
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit:focus {
|
||||||
|
border-color: @{blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit::placeholder {
|
||||||
|
color: @{surface2};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Combo box ───────────────────────────────────────────────────────────── */
|
||||||
|
QComboBox {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox:focus {
|
||||||
|
border-color: @{blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox::drop-down {
|
||||||
|
border: none;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background-color: @{mantle};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
selection-background-color: @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Spin box ────────────────────────────────────────────────────────────── */
|
||||||
|
QSpinBox {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSpinBox:focus { border-color: @{blue}; }
|
||||||
|
|
||||||
|
QSpinBox::up-button, QSpinBox::down-button {
|
||||||
|
background-color: @{surface1};
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Check box ───────────────────────────────────────────────────────────── */
|
||||||
|
QCheckBox {
|
||||||
|
color: @{text};
|
||||||
|
spacing: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox::indicator {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid @{surface1};
|
||||||
|
border-radius: 4px;
|
||||||
|
background: @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox::indicator:checked {
|
||||||
|
background-color: @{blue};
|
||||||
|
border-color: @{blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dialog ──────────────────────────────────────────────────────────────── */
|
||||||
|
QDialog {
|
||||||
|
background-color: @{base};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDialogButtonBox QPushButton {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Form layout labels ──────────────────────────────────────────────────── */
|
||||||
|
QFormLayout QLabel {
|
||||||
|
color: @{subtext};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab widget in dialogs ───────────────────────────────────────────────── */
|
||||||
|
QTabWidget#dialogTabs::pane {
|
||||||
|
border: 1px solid @{surface0};
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Group box ───────────────────────────────────────────────────────────── */
|
||||||
|
QGroupBox {
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1em;
|
||||||
|
color: @{subtext};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
left: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress bar ────────────────────────────────────────────────────────── */
|
||||||
|
QProgressBar {
|
||||||
|
background-color: @{surface0};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: @{blue};
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dock widget ─────────────────────────────────────────────────────────── */
|
||||||
|
QDockWidget {
|
||||||
|
color: @{text};
|
||||||
|
titlebar-close-icon: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDockWidget::title {
|
||||||
|
background-color: @{mantle};
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tool button ─────────────────────────────────────────────────────────── */
|
||||||
|
QToolButton {
|
||||||
|
background-color: @{surface0};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QToolButton:hover {
|
||||||
|
background-color: @{surface1};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Message box ─────────────────────────────────────────────────────────── */
|
||||||
|
QMessageBox {
|
||||||
|
background-color: @{base};
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox QLabel {
|
||||||
|
color: @{text};
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty workspace label ───────────────────────────────────────────────── */
|
||||||
|
#emptyLabel {
|
||||||
|
color: @{surface2};
|
||||||
|
font-size: 14pt;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status label in results toolbar ────────────────────────────────────── */
|
||||||
|
#statusLabel {
|
||||||
|
color: @{subtext};
|
||||||
|
font-size: 9pt;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── DB label in SQL editor toolbar ─────────────────────────────────────── */
|
||||||
|
#dbLabel {
|
||||||
|
color: @{overlay};
|
||||||
|
font-size: 9pt;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Workspace tab widget ────────────────────────────────────────────────── */
|
||||||
|
#workspace > QTabBar::tab {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Horizontal separator line in dialog ────────────────────────────────── */
|
||||||
|
QFrame[frameShape="4"] { /* HLine */
|
||||||
|
color: @{surface0};
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Structure view title ────────────────────────────────────────────────── */
|
||||||
|
#structureTitle {
|
||||||
|
color: @{lavender};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Pagination bar ──────────────────────────────────────────────────────── */
|
||||||
|
#paginationBar {
|
||||||
|
background-color: @{mantle};
|
||||||
|
border-top: 1px solid @{surface1};
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgBtn {
|
||||||
|
background-color: @{btn_pressed};
|
||||||
|
color: @{text};
|
||||||
|
border: 1px solid @{surface1};
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 9pt;
|
||||||
|
min-width: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgBtn:hover {
|
||||||
|
background-color: @{surface0};
|
||||||
|
border-color: @{surface2};
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgBtn:disabled {
|
||||||
|
color: @{surface1};
|
||||||
|
background-color: @{base};
|
||||||
|
border-color: @{surface0};
|
||||||
|
}
|
||||||
|
|
||||||
|
#pageLbl {
|
||||||
|
color: @{blue};
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rowCountLbl {
|
||||||
|
color: @{subtext};
|
||||||
|
font-size: 9pt;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user