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:
@@ -144,6 +144,7 @@ All four drivers have a `_where(where: dict) → (clause_str, params)` static he
|
|||||||
- Connection ping indicator: 30-second `_PingWorker` (QThread) pings each active connection via `test_connection()`; status dot (● green / ● red) shown in status bar right side with per-connection tooltip
|
- Connection ping indicator: 30-second `_PingWorker` (QThread) pings each active connection via `test_connection()`; status dot (● green / ● red) shown in status bar right side with per-connection tooltip
|
||||||
- Frozen first column in `TableViewer`: 📌 toggle button in toolbar; dual-view overlay (`_frozen_view` child of `_table_view`) with synced vertical scroll and row heights; event-filter updates geometry on resize
|
- Frozen first column in `TableViewer`: 📌 toggle button in toolbar; dual-view overlay (`_frozen_view` child of `_table_view`) with synced vertical scroll and row heights; event-filter updates geometry on resize
|
||||||
- Keyboard navigation in dialogs: OK set as default button (Enter submits) in `RowDialog` and `ConnectionDialog`; focus jumps to first input field on open; Enter in password field submits `ConnectionDialog`
|
- Keyboard navigation in dialogs: OK set as default button (Enter submits) in `RowDialog` and `ConnectionDialog`; focus jumps to first input field on open; Enter in password field submits `ConnectionDialog`
|
||||||
|
- Light / dark theme toggle: `app/config/theme.py` holds Catppuccin Frappé (dark) + Latte (light) palettes; `resources/style_light.qss` is the full Latte QSS; toggle via View → Switch Theme (Ctrl+Shift+T) or Preferences → Color theme; on switch, QSS is reloaded, syntax highlighter rebuilds rules via `update_theme()`, completer popup recolors, and model dirty/NULL colors update live
|
||||||
|
|
||||||
### Not Yet Implemented
|
### Not Yet Implemented
|
||||||
**High impact:**
|
**High impact:**
|
||||||
@@ -155,5 +156,4 @@ All four drivers have a `_where(where: dict) → (clause_str, params)` static he
|
|||||||
**Medium impact:**
|
**Medium impact:**
|
||||||
- Transaction panel: explicit BEGIN / COMMIT / ROLLBACK controls with in-flight indicator
|
- Transaction panel: explicit BEGIN / COMMIT / ROLLBACK controls with in-flight indicator
|
||||||
- Global schema search: search across all tables/columns/procedures in a database
|
- Global schema search: search across all tables/columns/procedures in a database
|
||||||
- Light theme: Catppuccin Latte or similar, toggle from Preferences
|
|
||||||
- Schema diff: compare two database schemas and show structural differences
|
- Schema diff: compare two database schemas and show structural differences
|
||||||
|
|||||||
@@ -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.QtCore import Qt, QTimer, QThread, pyqtSignal
|
||||||
from PyQt6.QtGui import QAction, QColor, QKeySequence
|
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.schema_browser import SchemaBrowser
|
||||||
from app.ui.sql_editor import SQLEditorWidget
|
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("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("New SQL Tab", self._new_sql_tab, "Ctrl+T"))
|
||||||
view_menu.addAction(self._act("Close Tab", self._close_current_tab, "Ctrl+W"))
|
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 ─────────────────────────────────────────────────────────────
|
||||||
tools_menu = mb.addMenu("&Tools")
|
tools_menu = mb.addMenu("&Tools")
|
||||||
@@ -662,9 +669,29 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _open_preferences(self):
|
def _open_preferences(self):
|
||||||
dlg = PreferencesDialog(parent=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()
|
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):
|
def _apply_settings_to_open_tabs(self):
|
||||||
"""Push updated settings to all currently open workspace tabs."""
|
"""Push updated settings to all currently open workspace tabs."""
|
||||||
for i in range(self._workspace.count()):
|
for i in range(self._workspace.count()):
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ from PyQt6.QtCore import (
|
|||||||
)
|
)
|
||||||
from PyQt6.QtGui import QColor, QFont
|
from PyQt6.QtGui import QColor, QFont
|
||||||
|
|
||||||
|
from app.config.theme import get_palette
|
||||||
|
|
||||||
|
|
||||||
class ResultTableModel(QAbstractTableModel):
|
class ResultTableModel(QAbstractTableModel):
|
||||||
"""Immutable result-set model — replaces data via set_data()."""
|
"""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
|
NUM_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||||
TEXT_ALIGN = Qt.AlignmentFlag.AlignLeft | 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"]
|
_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é)"),
|
||||||
|
("light", "☀️ Light (Catppuccin Latte)")]
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(QDialog):
|
class PreferencesDialog(QDialog):
|
||||||
@@ -64,6 +66,18 @@ class PreferencesDialog(QDialog):
|
|||||||
|
|
||||||
root.addWidget(results_box)
|
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 ───────────────────────────────────────────────────────────
|
||||||
general_box = QGroupBox("General")
|
general_box = QGroupBox("General")
|
||||||
gf = QFormLayout(general_box)
|
gf = QFormLayout(general_box)
|
||||||
@@ -82,7 +96,7 @@ class PreferencesDialog(QDialog):
|
|||||||
root.addStretch()
|
root.addStretch()
|
||||||
|
|
||||||
# ── Buttons ───────────────────────────────────────────────────────────
|
# ── Buttons ───────────────────────────────────────────────────────────
|
||||||
note = QLabel("Font changes take effect immediately in open editors.")
|
note = QLabel("Theme and font changes take effect immediately.")
|
||||||
note.setObjectName("statusLabel")
|
note.setObjectName("statusLabel")
|
||||||
root.addWidget(note)
|
root.addWidget(note)
|
||||||
|
|
||||||
@@ -116,6 +130,10 @@ class PreferencesDialog(QDialog):
|
|||||||
self._max_history.setValue(s.get("max_history", 500))
|
self._max_history.setValue(s.get("max_history", 500))
|
||||||
self._auto_commit.setChecked(s.get("auto_commit", True))
|
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):
|
def _apply(self):
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
s.set("font_family", self._font_family.currentText())
|
s.set("font_family", self._font_family.currentText())
|
||||||
@@ -125,6 +143,7 @@ class PreferencesDialog(QDialog):
|
|||||||
s.set("query_timeout", self._timeout.value())
|
s.set("query_timeout", self._timeout.value())
|
||||||
s.set("max_history", self._max_history.value())
|
s.set("max_history", self._max_history.value())
|
||||||
s.set("auto_commit", self._auto_commit.isChecked())
|
s.set("auto_commit", self._auto_commit.isChecked())
|
||||||
|
s.set("theme", _THEMES[self._theme.currentIndex()][0])
|
||||||
s.save()
|
s.save()
|
||||||
self.settings_applied.emit()
|
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.ui.syntax_highlighter import _KEYWORDS, _TYPES, _FUNCTIONS
|
||||||
from app.utils.worker import SchemaWorker
|
from app.utils.worker import SchemaWorker
|
||||||
|
from app.config.theme import get_palette
|
||||||
|
|
||||||
# Combined, deduplicated keyword list used as the static completion pool.
|
# Combined, deduplicated keyword list used as the static completion pool.
|
||||||
SQL_KEYWORDS: list[str] = list(dict.fromkeys(_KEYWORDS + _TYPES + _FUNCTIONS))
|
SQL_KEYWORDS: list[str] = list(dict.fromkeys(_KEYWORDS + _TYPES + _FUNCTIONS))
|
||||||
@@ -51,24 +52,28 @@ class _CompletionPopup(QListWidget):
|
|||||||
self.setFont(font)
|
self.setFont(font)
|
||||||
self._row_h = QFontMetrics(font).height() + 6
|
self._row_h = QFontMetrics(font).height() + 6
|
||||||
|
|
||||||
self.setStyleSheet("""
|
self._apply_popup_style()
|
||||||
QListWidget {
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||||
background: #292c3c;
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
color: #c6d0f5;
|
|
||||||
border: 1px solid #51576d;
|
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;
|
border-radius: 4px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}}
|
||||||
QListWidget::item { padding: 2px 8px; }
|
QListWidget::item {{ padding: 2px 8px; }}
|
||||||
QListWidget::item:selected {
|
QListWidget::item:selected {{
|
||||||
background: #414559;
|
background: {p['popup_selected']};
|
||||||
color: #c6d0f5;
|
color: {p['text']};
|
||||||
}
|
}}
|
||||||
QListWidget::item:hover { background: #363a4f; }
|
QListWidget::item:hover {{ background: {p['popup_hover']}; }}
|
||||||
""")
|
""")
|
||||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
||||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
||||||
|
|
||||||
self.itemClicked.connect(
|
self.itemClicked.connect(
|
||||||
lambda item: self._editor._completer._accept_completion(item.text())
|
lambda item: self._editor._completer._accept_completion(item.text())
|
||||||
@@ -142,6 +147,10 @@ class SqlCompleter(QObject):
|
|||||||
|
|
||||||
# ── Public API ────────────────────────────────────────────────────────────
|
# ── 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:
|
def set_context(self, driver, database: str) -> None:
|
||||||
"""Update the active driver/database and refresh schema cache."""
|
"""Update the active driver/database and refresh schema cache."""
|
||||||
self._driver = driver
|
self._driver = driver
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ class EditorTab(QWidget):
|
|||||||
self._splitter.setHandleWidth(3)
|
self._splitter.setHandleWidth(3)
|
||||||
|
|
||||||
self._editor = CodeEditor()
|
self._editor = CodeEditor()
|
||||||
SQLHighlighter(self._editor.document())
|
self._highlighter = SQLHighlighter(self._editor.document())
|
||||||
self._completer = SqlCompleter(self._editor, parent=self)
|
self._completer = SqlCompleter(self._editor, parent=self)
|
||||||
self._editor.set_completer(self._completer)
|
self._editor.set_completer(self._completer)
|
||||||
if self._driver:
|
if self._driver:
|
||||||
@@ -603,6 +603,8 @@ class EditorTab(QWidget):
|
|||||||
|
|
||||||
def apply_settings(self) -> None:
|
def apply_settings(self) -> None:
|
||||||
self._editor.apply_settings()
|
self._editor.apply_settings()
|
||||||
|
self._highlighter.update_theme()
|
||||||
|
self._completer.update_theme()
|
||||||
|
|
||||||
|
|
||||||
# ── Tabbed SQL editor container ───────────────────────────────────────────────
|
# ── Tabbed SQL editor container ───────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
SQL syntax highlighter for QPlainTextEdit.
|
SQL syntax highlighter for QPlainTextEdit.
|
||||||
Highlights keywords, types, functions, strings, comments, and numbers.
|
Highlights keywords, types, functions, strings, comments, and numbers.
|
||||||
|
Colors follow the active Catppuccin theme (dark = Frappé, light = Latte).
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from PyQt6.QtCore import QRegularExpression, Qt
|
from PyQt6.QtCore import QRegularExpression, Qt
|
||||||
@@ -8,6 +9,8 @@ from PyQt6.QtGui import (
|
|||||||
QSyntaxHighlighter, QTextCharFormat, QColor, QFont
|
QSyntaxHighlighter, QTextCharFormat, QColor, QFont
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app.config.theme import get_palette
|
||||||
|
|
||||||
|
|
||||||
def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
||||||
f = QTextCharFormat()
|
f = QTextCharFormat()
|
||||||
@@ -17,17 +20,7 @@ def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharForma
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
# ── Token categories (Catppuccin Frappé palette) ─────────────────────────────
|
# ── Keyword lists (shared with completer) ─────────────────────────────────────
|
||||||
_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 ─────────────────────────────────────────────────────────────
|
|
||||||
_KEYWORDS = [
|
_KEYWORDS = [
|
||||||
"SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER",
|
"SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER",
|
||||||
"FULL", "CROSS", "ON", "AS", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
|
"FULL", "CROSS", "ON", "AS", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
|
||||||
@@ -79,56 +72,56 @@ class SQLHighlighter(QSyntaxHighlighter):
|
|||||||
def __init__(self, document):
|
def __init__(self, document):
|
||||||
super().__init__(document)
|
super().__init__(document)
|
||||||
self._rules: list[tuple] = []
|
self._rules: list[tuple] = []
|
||||||
|
self._build_rules()
|
||||||
|
|
||||||
|
def _build_rules(self):
|
||||||
|
p = get_palette()
|
||||||
|
|
||||||
def kw_pattern(words: list[str]) -> str:
|
def kw_pattern(words: list[str]) -> str:
|
||||||
return r"\b(?:" + "|".join(words) + r")\b"
|
return r"\b(?:" + "|".join(words) + r")\b"
|
||||||
|
|
||||||
|
ci = QRegularExpression.PatternOption.CaseInsensitiveOption
|
||||||
self._rules = [
|
self._rules = [
|
||||||
# Keywords (case-insensitive handled via flag)
|
(QRegularExpression(kw_pattern(_KEYWORDS), ci),
|
||||||
(QRegularExpression(kw_pattern(_KEYWORDS),
|
_fmt(p["syn_keyword"], bold=True)),
|
||||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
(QRegularExpression(kw_pattern(_TYPES), ci),
|
||||||
_KEYWORD_FMT),
|
_fmt(p["syn_type"])),
|
||||||
# Data types
|
(QRegularExpression(kw_pattern(_FUNCTIONS), ci),
|
||||||
(QRegularExpression(kw_pattern(_TYPES),
|
_fmt(p["syn_function"])),
|
||||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
(QRegularExpression(r"\b\d+(\.\d+)?\b"),
|
||||||
_TYPE_FMT),
|
_fmt(p["syn_number"])),
|
||||||
# Functions
|
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"),
|
||||||
(QRegularExpression(kw_pattern(_FUNCTIONS),
|
_fmt(p["syn_string"])),
|
||||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
(QRegularExpression(r'"[^"]*"'),
|
||||||
_FUNCTION_FMT),
|
_fmt(p["syn_ident"])),
|
||||||
# Numbers
|
(QRegularExpression(r"`[^`]*`"),
|
||||||
(QRegularExpression(r"\b\d+(\.\d+)?\b"), _NUMBER_FMT),
|
_fmt(p["syn_ident"])),
|
||||||
# Single-quoted strings
|
(QRegularExpression(
|
||||||
(QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"), _STRING_FMT),
|
r"\bSELECT\s+\*|\*(?=\s*FROM)", ci),
|
||||||
# Double-quoted identifiers
|
_fmt(p["syn_operator"], bold=True)),
|
||||||
(QRegularExpression(r'"[^"]*"'), _fmt("#99d1db")),
|
(QRegularExpression(r"[=<>!%&|^~+\-*/]"),
|
||||||
# Backtick identifiers (MySQL)
|
_fmt(p["syn_operator"])),
|
||||||
(QRegularExpression(r"`[^`]*`"), _fmt("#99d1db")),
|
(QRegularExpression(r"--[^\n]*"),
|
||||||
# * wildcard
|
_fmt(p["syn_comment"], italic=True)),
|
||||||
(QRegularExpression(r"\bSELECT\s+\*|\*(?=\s*FROM)",
|
(QRegularExpression(r"#[^\n]*"),
|
||||||
QRegularExpression.PatternOption.CaseInsensitiveOption),
|
_fmt(p["syn_comment"], italic=True)),
|
||||||
_STAR_FMT),
|
|
||||||
# Operators
|
|
||||||
(QRegularExpression(r"[=<>!%&|^~+\-*/]"), _OPERATOR_FMT),
|
|
||||||
# Single-line comment --
|
|
||||||
(QRegularExpression(r"--[^\n]*"), _COMMENT_FMT),
|
|
||||||
# Single-line comment #
|
|
||||||
(QRegularExpression(r"#[^\n]*"), _COMMENT_FMT),
|
|
||||||
]
|
]
|
||||||
|
self._comment_fmt = _fmt(p["syn_comment"], italic=True)
|
||||||
# Multi-line block comment /* ... */
|
|
||||||
self._block_comment_start = QRegularExpression(r"/\*")
|
self._block_comment_start = QRegularExpression(r"/\*")
|
||||||
self._block_comment_end = 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:
|
def highlightBlock(self, text: str) -> None:
|
||||||
# Single-line rules
|
|
||||||
for pattern, fmt in self._rules:
|
for pattern, fmt in self._rules:
|
||||||
it = pattern.globalMatch(text)
|
it = pattern.globalMatch(text)
|
||||||
while it.hasNext():
|
while it.hasNext():
|
||||||
m = it.next()
|
m = it.next()
|
||||||
self.setFormat(m.capturedStart(), m.capturedLength(), fmt)
|
self.setFormat(m.capturedStart(), m.capturedLength(), fmt)
|
||||||
|
|
||||||
# Multi-line block comments
|
|
||||||
self.setCurrentBlockState(0)
|
self.setCurrentBlockState(0)
|
||||||
start_idx = 0
|
start_idx = 0
|
||||||
if self.previousBlockState() != 1:
|
if self.previousBlockState() != 1:
|
||||||
@@ -139,10 +132,10 @@ class SQLHighlighter(QSyntaxHighlighter):
|
|||||||
end_m = self._block_comment_end.match(text, start_idx)
|
end_m = self._block_comment_end.match(text, start_idx)
|
||||||
if end_m.hasMatch():
|
if end_m.hasMatch():
|
||||||
end_idx = end_m.capturedStart() + end_m.capturedLength()
|
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)
|
m = self._block_comment_start.match(text, end_idx)
|
||||||
start_idx = m.capturedStart() if m.hasMatch() else -1
|
start_idx = m.capturedStart() if m.hasMatch() else -1
|
||||||
else:
|
else:
|
||||||
self.setCurrentBlockState(1)
|
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
|
break
|
||||||
|
|||||||
+15
-6
@@ -22,6 +22,7 @@ from PyQt6.QtGui import QColor, QBrush, QFont, QKeySequence, QShortcut
|
|||||||
|
|
||||||
from app.utils.worker import TableDataWorker
|
from app.utils.worker import TableDataWorker
|
||||||
from app.config.settings import get_settings
|
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)
|
return "" if val is None else str(val)
|
||||||
|
|
||||||
if role == Qt.ItemDataRole.ForegroundRole:
|
if role == Qt.ItemDataRole.ForegroundRole:
|
||||||
|
p = get_palette()
|
||||||
if self._is_deleted_row(r):
|
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:
|
if (r, c) in self._dirty:
|
||||||
return QBrush(QColor("#ef9f76")) # orange — edited
|
return QBrush(QColor(p["dirty_fg"]))
|
||||||
if val is None:
|
if val is None:
|
||||||
return QBrush(QColor("#737994")) # grey — NULL
|
return QBrush(QColor(p["overlay"]))
|
||||||
|
|
||||||
if role == Qt.ItemDataRole.BackgroundRole:
|
if role == Qt.ItemDataRole.BackgroundRole:
|
||||||
|
p = get_palette()
|
||||||
if self._is_deleted_row(r):
|
if self._is_deleted_row(r):
|
||||||
return QBrush(QColor("#3a2530"))
|
return QBrush(QColor(p["delete_bg"]))
|
||||||
if (r, c) in self._dirty:
|
if (r, c) in self._dirty:
|
||||||
return QBrush(QColor("#3a2f25"))
|
return QBrush(QColor(p["dirty_bg"]))
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -444,7 +447,7 @@ class TableViewer(QWidget):
|
|||||||
self._frozen_view.horizontalHeader().setSectionResizeMode(
|
self._frozen_view.horizontalHeader().setSectionResizeMode(
|
||||||
QHeaderView.ResizeMode.Fixed)
|
QHeaderView.ResizeMode.Fixed)
|
||||||
self._frozen_view.setStyleSheet(
|
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()
|
self._frozen_view.hide()
|
||||||
|
|
||||||
# Sync vertical scroll between the two views
|
# Sync vertical scroll between the two views
|
||||||
@@ -887,6 +890,12 @@ class TableViewer(QWidget):
|
|||||||
)
|
)
|
||||||
QApplication.clipboard().setText("\n".join(statements))
|
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):
|
def _filter_by_cell(self, index):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,15 +9,13 @@ from PyQt6.QtGui import QFont
|
|||||||
|
|
||||||
from app.utils.logger import setup_logging, install_qt_message_handler, get_logger
|
from app.utils.logger import setup_logging, install_qt_message_handler, get_logger
|
||||||
from app.main_window import MainWindow
|
from app.main_window import MainWindow
|
||||||
|
from app.config.theme import apply_qss
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def load_stylesheet(app: QApplication) -> None:
|
def load_stylesheet(app: QApplication) -> None:
|
||||||
style_path = os.path.join(os.path.dirname(__file__), "resources", "style.qss")
|
apply_qss(app) # reads theme from settings, falls back to dark
|
||||||
if os.path.exists(style_path):
|
|
||||||
with open(style_path, "r", encoding="utf-8") as f:
|
|
||||||
app.setStyleSheet(f.read())
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -0,0 +1,599 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
DBClient — Light Theme (Catppuccin Latte palette)
|
||||||
|
═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Variables via flat tokens ─────────────────────────────────────────────
|
||||||
|
base #eff1f5 crust #dce0e8
|
||||||
|
surface0 #ccd0da surface1 #bcc0cc surface2 #acb0be
|
||||||
|
overlay #9ca0b0 subtext #6c6f85 text #4c4f69
|
||||||
|
blue #1e66f5 lavender #7287fd sapphire #209fb5
|
||||||
|
green #40a02b teal #179299 sky #04a5e5
|
||||||
|
mauve #8839ef pink #ea76cb red #d20f39
|
||||||
|
peach #fe640b yellow #df8e1d
|
||||||
|
─────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* ── Global reset ───────────────────────────────────────────────────────── */
|
||||||
|
* {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMainWindow, QDialog {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
color: #4c4f69;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
color: #4c4f69;
|
||||||
|
font-family: "Segoe UI", "Inter", sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Menu bar ───────────────────────────────────────────────────────────── */
|
||||||
|
QMenuBar {
|
||||||
|
background-color: #dce0e8;
|
||||||
|
color: #4c4f69;
|
||||||
|
border-bottom: 1px solid #ccd0da;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenuBar::item {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenuBar::item:selected {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu::item {
|
||||||
|
padding: 6px 24px 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #4c4f69;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu::item:selected {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #4c4f69;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu::separator {
|
||||||
|
height: 1px;
|
||||||
|
background: #bcc0cc;
|
||||||
|
margin: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status bar ─────────────────────────────────────────────────────────── */
|
||||||
|
QStatusBar {
|
||||||
|
background-color: #dce0e8;
|
||||||
|
border-top: 1px solid #ccd0da;
|
||||||
|
color: #6c6f85;
|
||||||
|
font-size: 9pt;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scrollbars ─────────────────────────────────────────────────────────── */
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background: #eff1f5;
|
||||||
|
width: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background: #bcc0cc;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical:hover {
|
||||||
|
background: #acb0be;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar:horizontal {
|
||||||
|
background: #eff1f5;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal {
|
||||||
|
background: #bcc0cc;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal:hover {
|
||||||
|
background: #acb0be;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; }
|
||||||
|
QScrollBar::add-page, QScrollBar::sub-page { background: transparent; }
|
||||||
|
|
||||||
|
/* ── Splitter ────────────────────────────────────────────────────────────── */
|
||||||
|
QSplitter::handle {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSplitter::handle:horizontal { width: 2px; }
|
||||||
|
QSplitter::handle:vertical { height: 2px; }
|
||||||
|
|
||||||
|
QSplitter::handle:hover {
|
||||||
|
background-color: #1e66f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar ─────────────────────────────────────────────────────────────── */
|
||||||
|
#sidebarHeader {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
border-bottom: 1px solid #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarTitle {
|
||||||
|
color: #1e66f5;
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#newConnBtn {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #1e66f5;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14pt;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#newConnBtn:hover {
|
||||||
|
background-color: #bcc0cc;
|
||||||
|
color: #7287fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tree widget ─────────────────────────────────────────────────────────── */
|
||||||
|
QTreeWidget {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
border: none;
|
||||||
|
color: #4c4f69;
|
||||||
|
font-size: 10pt;
|
||||||
|
show-decoration-selected: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::item {
|
||||||
|
padding: 3px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::item:hover {
|
||||||
|
background-color: #d8dce8;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::item:selected {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #4c4f69;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTreeWidget::branch {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab widget ──────────────────────────────────────────────────────────── */
|
||||||
|
QTabWidget::pane {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #ccd0da;
|
||||||
|
background-color: #eff1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::tab {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
color: #6c6f85;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid #ccd0da;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::tab:selected {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
color: #4c4f69;
|
||||||
|
border-bottom: 2px solid #1e66f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::tab:hover:!selected {
|
||||||
|
background-color: #d0d4e0;
|
||||||
|
color: #4c4f69;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabBar::close-button {
|
||||||
|
subcontrol-position: right;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Table view ──────────────────────────────────────────────────────────── */
|
||||||
|
QTableView, QTableWidget {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
alternate-background-color: #e6e9ef;
|
||||||
|
gridline-color: #ccd0da;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: none;
|
||||||
|
selection-background-color: #ccd0da;
|
||||||
|
selection-color: #4c4f69;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTableView::item, QTableWidget::item {
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView::section {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
color: #1e66f5;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid #ccd0da;
|
||||||
|
border-bottom: 1px solid #ccd0da;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView::section:hover {
|
||||||
|
background-color: #d0d4e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Plain text edit (SQL editor body) ───────────────────────────────────── */
|
||||||
|
QPlainTextEdit {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: none;
|
||||||
|
selection-background-color: #bcc0cc;
|
||||||
|
font-family: "Consolas", "JetBrains Mono", "Courier New", monospace;
|
||||||
|
font-size: 13pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Line number gutter ──────────────────────────────────────────────────── */
|
||||||
|
LineNumberArea {
|
||||||
|
background-color: #dce0e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Buttons ─────────────────────────────────────────────────────────────── */
|
||||||
|
QPushButton {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 14px;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #bcc0cc;
|
||||||
|
border-color: #acb0be;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #d0d4de;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:disabled {
|
||||||
|
color: #acb0be;
|
||||||
|
background-color: #d0d4de;
|
||||||
|
border-color: #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
#runBtn {
|
||||||
|
background-color: #40a02b;
|
||||||
|
color: #eff1f5;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#runBtn:hover {
|
||||||
|
background-color: #179299;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopBtn {
|
||||||
|
background-color: #d20f39;
|
||||||
|
color: #eff1f5;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopBtn:hover {
|
||||||
|
background-color: #e5394b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#commitBtn {
|
||||||
|
background-color: #40a02b;
|
||||||
|
color: #eff1f5;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rollbackBtn {
|
||||||
|
background-color: #fe640b;
|
||||||
|
color: #eff1f5;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudAddBtn {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #40a02b;
|
||||||
|
border: 1px solid #40a02b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudAddBtn:hover {
|
||||||
|
background-color: #40a02b;
|
||||||
|
color: #eff1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudDeleteBtn {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #d20f39;
|
||||||
|
border: 1px solid #d20f39;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#crudDeleteBtn:hover {
|
||||||
|
background-color: #d20f39;
|
||||||
|
color: #eff1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Line edit ───────────────────────────────────────────────────────────── */
|
||||||
|
QLineEdit {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 10pt;
|
||||||
|
selection-background-color: #1e66f5;
|
||||||
|
selection-color: #eff1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit:focus {
|
||||||
|
border-color: #1e66f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit::placeholder {
|
||||||
|
color: #acb0be;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Combo box ───────────────────────────────────────────────────────────── */
|
||||||
|
QComboBox {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox:focus {
|
||||||
|
border-color: #1e66f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox::drop-down {
|
||||||
|
border: none;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
selection-background-color: #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Spin box ────────────────────────────────────────────────────────────── */
|
||||||
|
QSpinBox {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSpinBox:focus { border-color: #1e66f5; }
|
||||||
|
|
||||||
|
QSpinBox::up-button, QSpinBox::down-button {
|
||||||
|
background-color: #bcc0cc;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Check box ───────────────────────────────────────────────────────────── */
|
||||||
|
QCheckBox {
|
||||||
|
color: #4c4f69;
|
||||||
|
spacing: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox::indicator {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #bcc0cc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #e6e9ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox::indicator:checked {
|
||||||
|
background-color: #1e66f5;
|
||||||
|
border-color: #1e66f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dialog ──────────────────────────────────────────────────────────────── */
|
||||||
|
QDialog {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDialogButtonBox QPushButton {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Form layout labels ──────────────────────────────────────────────────── */
|
||||||
|
QFormLayout QLabel {
|
||||||
|
color: #6c6f85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab widget in dialogs ───────────────────────────────────────────────── */
|
||||||
|
QTabWidget#dialogTabs::pane {
|
||||||
|
border: 1px solid #ccd0da;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Group box ───────────────────────────────────────────────────────────── */
|
||||||
|
QGroupBox {
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1em;
|
||||||
|
color: #6c6f85;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
left: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress bar ────────────────────────────────────────────────────────── */
|
||||||
|
QProgressBar {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #1e66f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dock widget ─────────────────────────────────────────────────────────── */
|
||||||
|
QDockWidget {
|
||||||
|
color: #4c4f69;
|
||||||
|
titlebar-close-icon: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDockWidget::title {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tool button ─────────────────────────────────────────────────────────── */
|
||||||
|
QToolButton {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QToolButton:hover {
|
||||||
|
background-color: #bcc0cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Message box ─────────────────────────────────────────────────────────── */
|
||||||
|
QMessageBox {
|
||||||
|
background-color: #eff1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox QLabel {
|
||||||
|
color: #4c4f69;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty workspace label ───────────────────────────────────────────────── */
|
||||||
|
#emptyLabel {
|
||||||
|
color: #acb0be;
|
||||||
|
font-size: 14pt;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status label in results toolbar ────────────────────────────────────── */
|
||||||
|
#statusLabel {
|
||||||
|
color: #6c6f85;
|
||||||
|
font-size: 9pt;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── DB label in SQL editor toolbar ─────────────────────────────────────── */
|
||||||
|
#dbLabel {
|
||||||
|
color: #9ca0b0;
|
||||||
|
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: #ccd0da;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Structure view title ────────────────────────────────────────────────── */
|
||||||
|
#structureTitle {
|
||||||
|
color: #7287fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Pagination bar ──────────────────────────────────────────────────────── */
|
||||||
|
#paginationBar {
|
||||||
|
background-color: #e6e9ef;
|
||||||
|
border-top: 1px solid #bcc0cc;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgBtn {
|
||||||
|
background-color: #d0d4de;
|
||||||
|
color: #4c4f69;
|
||||||
|
border: 1px solid #bcc0cc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 9pt;
|
||||||
|
min-width: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgBtn:hover {
|
||||||
|
background-color: #ccd0da;
|
||||||
|
border-color: #acb0be;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgBtn:disabled {
|
||||||
|
color: #bcc0cc;
|
||||||
|
background-color: #eff1f5;
|
||||||
|
border-color: #ccd0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pageLbl {
|
||||||
|
color: #1e66f5;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rowCountLbl {
|
||||||
|
color: #6c6f85;
|
||||||
|
font-size: 9pt;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user