Commit working theme changes
This commit is contained in:
parent
a7c8cc5dbf
commit
c3b83b0238
9 changed files with 363 additions and 62 deletions
|
|
@ -4,6 +4,7 @@
|
|||
* Fix styling issue with text that comes after a URL, so it doesn't appear as part of the URL.
|
||||
* Add ability to export to Markdown (and fix heading styles)
|
||||
* Represent in the History diff pane when an image was the thing that changed
|
||||
* Support theme choice in settings (light/dark/system)
|
||||
|
||||
# 0.1.9
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class DBConfig:
|
|||
path: Path
|
||||
key: str
|
||||
idle_minutes: int = 15 # 0 = never lock
|
||||
theme: str = "system"
|
||||
|
||||
|
||||
class DBManager:
|
||||
|
|
@ -160,13 +161,6 @@ class DBManager:
|
|||
).fetchone()
|
||||
return row[0] if row else ""
|
||||
|
||||
def upsert_entry(self, date_iso: str, content: str) -> None:
|
||||
"""
|
||||
Insert or update an entry.
|
||||
"""
|
||||
# Make a new version and set it as current
|
||||
self.save_new_version(date_iso, content, note=None, set_current=True)
|
||||
|
||||
def search_entries(self, text: str) -> list[str]:
|
||||
"""
|
||||
Search for entries by term. This only works against the latest
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from PySide6.QtGui import (
|
|||
QFontDatabase,
|
||||
QImage,
|
||||
QImageReader,
|
||||
QPalette,
|
||||
QPixmap,
|
||||
QTextCharFormat,
|
||||
QTextCursor,
|
||||
|
|
@ -28,8 +29,11 @@ from PySide6.QtCore import (
|
|||
QBuffer,
|
||||
QByteArray,
|
||||
QIODevice,
|
||||
QTimer,
|
||||
)
|
||||
from PySide6.QtWidgets import QTextEdit
|
||||
from PySide6.QtWidgets import QTextEdit, QApplication
|
||||
|
||||
from .theme import Theme, ThemeManager
|
||||
|
||||
|
||||
class Editor(QTextEdit):
|
||||
|
|
@ -42,7 +46,7 @@ class Editor(QTextEdit):
|
|||
_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
|
||||
_DATA_IMG_RX = re.compile(r'src=["\']data:image/[^;]+;base64,([^"\']+)["\']', re.I)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, theme_manager: ThemeManager, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tab_w = 4 * self.fontMetrics().horizontalAdvance(" ")
|
||||
self.setTabStopDistance(tab_w)
|
||||
|
|
@ -55,7 +59,13 @@ class Editor(QTextEdit):
|
|||
|
||||
self.setAcceptRichText(True)
|
||||
|
||||
# Turn raw URLs into anchors
|
||||
# If older docs have a baked-in color, normalize once:
|
||||
self._retint_anchors_to_palette()
|
||||
|
||||
self._themes = theme_manager
|
||||
# Refresh on theme change
|
||||
self._themes.themeChanged.connect(self._on_theme_changed)
|
||||
|
||||
self._linkifying = False
|
||||
self.textChanged.connect(self._linkify_document)
|
||||
self.viewport().setMouseTracking(True)
|
||||
|
|
@ -87,15 +97,6 @@ class Editor(QTextEdit):
|
|||
f = f.parentFrame()
|
||||
return None
|
||||
|
||||
def _is_code_block(self, block) -> bool:
|
||||
if not block.isValid():
|
||||
return False
|
||||
bf = block.blockFormat()
|
||||
return bool(
|
||||
bf.nonBreakableLines()
|
||||
and bf.background().color().rgb() == self._CODE_BG.rgb()
|
||||
)
|
||||
|
||||
def _trim_url_end(self, url: str) -> str:
|
||||
# strip common trailing punctuation not part of the URL
|
||||
trimmed = url.rstrip(".,;:!?\"'")
|
||||
|
|
@ -141,7 +142,7 @@ class Editor(QTextEdit):
|
|||
fmt.setAnchor(True)
|
||||
fmt.setAnchorHref(href) # always refresh to the latest full URL
|
||||
fmt.setFontUnderline(True)
|
||||
fmt.setForeground(Qt.blue)
|
||||
fmt.setForeground(self.palette().brush(QPalette.Link))
|
||||
|
||||
cur.mergeCharFormat(fmt) # merge so we don’t clobber other styling
|
||||
|
||||
|
|
@ -481,11 +482,6 @@ class Editor(QTextEdit):
|
|||
# otherwise default handling
|
||||
return super().keyPressEvent(e)
|
||||
|
||||
def _clear_insertion_char_format(self):
|
||||
"""Reset inline typing format (keeps lists, alignment, margins, etc.)."""
|
||||
nf = QTextCharFormat()
|
||||
self.setCurrentCharFormat(nf)
|
||||
|
||||
def _break_anchor_for_next_char(self):
|
||||
"""
|
||||
Ensure the *next* typed character is not part of a hyperlink.
|
||||
|
|
@ -669,3 +665,41 @@ class Editor(QTextEdit):
|
|||
fmt = QTextListFormat()
|
||||
fmt.setStyle(QTextListFormat.Style.ListDecimal)
|
||||
c.createList(fmt)
|
||||
|
||||
@Slot(Theme)
|
||||
def _on_theme_changed(self, _theme: Theme):
|
||||
# Defer one event-loop tick so widgets have the new palette
|
||||
QTimer.singleShot(0, self._retint_anchors_to_palette)
|
||||
|
||||
@Slot()
|
||||
def _retint_anchors_to_palette(self, *_):
|
||||
# Always read from the *application* palette to avoid stale widget palette
|
||||
app = QApplication.instance()
|
||||
link_brush = app.palette().brush(QPalette.Link)
|
||||
doc = self.document()
|
||||
cur = QTextCursor(doc)
|
||||
cur.beginEditBlock()
|
||||
block = doc.firstBlock()
|
||||
while block.isValid():
|
||||
it = block.begin()
|
||||
while not it.atEnd():
|
||||
frag = it.fragment()
|
||||
if frag.isValid():
|
||||
fmt = frag.charFormat()
|
||||
if fmt.isAnchor():
|
||||
new_fmt = QTextCharFormat(fmt)
|
||||
new_fmt.setForeground(link_brush) # force palette link color
|
||||
cur.setPosition(frag.position())
|
||||
cur.setPosition(
|
||||
frag.position() + frag.length(), QTextCursor.KeepAnchor
|
||||
)
|
||||
cur.setCharFormat(new_fmt)
|
||||
it += 1
|
||||
block = block.next()
|
||||
cur.endEditBlock()
|
||||
self.viewport().update()
|
||||
|
||||
def setHtml(self, html: str) -> None:
|
||||
super().setHtml(html)
|
||||
# Ensure anchors adopt the palette color on startup
|
||||
self._retint_anchors_to_palette()
|
||||
|
|
|
|||
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
|||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from .settings import APP_NAME, APP_ORG
|
||||
from .settings import APP_NAME, APP_ORG, get_settings
|
||||
from .main_window import MainWindow
|
||||
from .theme import Theme, ThemeConfig, ThemeManager
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName(APP_NAME)
|
||||
app.setOrganizationName(APP_ORG)
|
||||
win = MainWindow()
|
||||
|
||||
s = get_settings()
|
||||
theme_str = s.value("ui/theme", "system")
|
||||
cfg = ThemeConfig(theme=Theme(theme_str))
|
||||
themes = ThemeManager(app, cfg)
|
||||
themes.apply(cfg.theme)
|
||||
|
||||
win = MainWindow(themes=themes)
|
||||
win.show()
|
||||
sys.exit(app.exec())
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ from PySide6.QtGui import (
|
|||
QDesktopServices,
|
||||
QFont,
|
||||
QGuiApplication,
|
||||
QPalette,
|
||||
QTextCharFormat,
|
||||
QTextListFormat,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCalendarWidget,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
|
|
@ -48,6 +51,7 @@ from .search import Search
|
|||
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
||||
from .settings_dialog import SettingsDialog
|
||||
from .toolbar import ToolBar
|
||||
from .theme import Theme, ThemeManager
|
||||
|
||||
|
||||
class _LockOverlay(QWidget):
|
||||
|
|
@ -58,23 +62,6 @@ class _LockOverlay(QWidget):
|
|||
self.setFocusPolicy(Qt.StrongFocus)
|
||||
self.setGeometry(parent.rect())
|
||||
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
#LockOverlay { background-color: #ccc; }
|
||||
#LockOverlay QLabel { color: #fff; font-size: 18px; }
|
||||
#LockOverlay QPushButton {
|
||||
background-color: #f2f2f2;
|
||||
color: #000;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid #808080;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
#LockOverlay QPushButton:hover { background-color: #ffffff; }
|
||||
#LockOverlay QPushButton:pressed { background-color: #e6e6e6; }
|
||||
"""
|
||||
)
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
lay.addStretch(1)
|
||||
|
||||
|
|
@ -92,8 +79,42 @@ class _LockOverlay(QWidget):
|
|||
lay.addWidget(self._btn, 0, Qt.AlignCenter)
|
||||
lay.addStretch(1)
|
||||
|
||||
self._apply_overlay_style()
|
||||
|
||||
self.hide() # start hidden
|
||||
|
||||
def _apply_overlay_style(self):
|
||||
pal = self.palette()
|
||||
bg = (
|
||||
pal.window().color().darker(180)
|
||||
if pal.color(QPalette.Window).value() < 128
|
||||
else pal.window().color().lighter(110)
|
||||
)
|
||||
text = pal.windowText().color()
|
||||
btn_bg = pal.button().color()
|
||||
btn_fg = pal.buttonText().color()
|
||||
border = pal.mid().color()
|
||||
|
||||
hover_bg = btn_bg.lighter(106) # +6%
|
||||
press_bg = btn_bg.darker(106) # -6%
|
||||
|
||||
self.setStyleSheet(
|
||||
f"""
|
||||
#LockOverlay {{ background-color: {bg.name()}; }}
|
||||
#LockOverlay QLabel {{ color: {text.name()}; font-size: 18px; }}
|
||||
#LockOverlay QPushButton {{
|
||||
background-color: {btn_bg.name()};
|
||||
color: {btn_fg.name()};
|
||||
padding: 6px 14px;
|
||||
border: 1px solid {border.name()};
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
#LockOverlay QPushButton:hover {{ background-color: {hover_bg.name()}; }}
|
||||
#LockOverlay QPushButton:pressed {{ background-color: {press_bg.name()}; }}
|
||||
"""
|
||||
)
|
||||
|
||||
# keep overlay sized with its parent
|
||||
def eventFilter(self, obj, event):
|
||||
if obj is self.parent() and event.type() in (QEvent.Resize, QEvent.Show):
|
||||
|
|
@ -106,11 +127,13 @@ class _LockOverlay(QWidget):
|
|||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, themes: ThemeManager, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setWindowTitle(APP_NAME)
|
||||
self.setMinimumSize(1000, 650)
|
||||
|
||||
self.themes = themes # Store the themes manager
|
||||
|
||||
self.cfg = load_db_config()
|
||||
if not os.path.exists(self.cfg.path):
|
||||
# Fresh database/first time use, so guide the user re: setting a key
|
||||
|
|
@ -145,7 +168,7 @@ class MainWindow(QMainWindow):
|
|||
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
||||
|
||||
# This is the note-taking editor
|
||||
self.editor = Editor()
|
||||
self.editor = Editor(self.themes)
|
||||
|
||||
# Toolbar for controlling styling
|
||||
self.toolBar = ToolBar()
|
||||
|
|
@ -185,6 +208,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
# full-window overlay that sits on top of the central widget
|
||||
self._lock_overlay = _LockOverlay(self.centralWidget(), self._on_unlock_clicked)
|
||||
self._lock_overlay._apply_overlay_style()
|
||||
self.centralWidget().installEventFilter(self._lock_overlay)
|
||||
|
||||
self._locked = False
|
||||
|
|
@ -280,6 +304,16 @@ class MainWindow(QMainWindow):
|
|||
self.settings = QSettings(APP_ORG, APP_NAME)
|
||||
self._restore_window_position()
|
||||
|
||||
self._apply_link_css() # Apply link color on startup
|
||||
# re-apply all runtime color tweaks when theme changes
|
||||
self.themes.themeChanged.connect(lambda _t: self._retheme_overrides())
|
||||
self.themes.themeChanged.connect(self._apply_calendar_theme)
|
||||
self._apply_calendar_text_colors()
|
||||
self._apply_calendar_theme(self.themes.current())
|
||||
|
||||
# apply once on startup so links / calendar colors are set immediately
|
||||
self._retheme_overrides()
|
||||
|
||||
def _try_connect(self) -> bool:
|
||||
"""
|
||||
Try to connect to the database.
|
||||
|
|
@ -314,6 +348,86 @@ class MainWindow(QMainWindow):
|
|||
if self._try_connect():
|
||||
return True
|
||||
|
||||
def _retheme_overrides(self):
|
||||
if hasattr(self, "_lock_overlay"):
|
||||
self._lock_overlay._apply_overlay_style()
|
||||
self._apply_calendar_text_colors()
|
||||
self._apply_link_css() # Reapply link styles based on the current theme
|
||||
self._apply_search_highlights(getattr(self, "_search_highlighted_dates", set()))
|
||||
self.calendar.update()
|
||||
self.editor.viewport().update()
|
||||
|
||||
def _apply_link_css(self):
|
||||
if self.themes and self.themes.current() == Theme.DARK:
|
||||
anchor = "#FFA500" # Orange links
|
||||
visited = "#B38000" # Visited links color
|
||||
css = f"""
|
||||
a {{ color: {anchor}; text-decoration: underline; }}
|
||||
a:visited {{ color: {visited}; }}
|
||||
"""
|
||||
else:
|
||||
css = "" # Default to no custom styling for links (system or light theme)
|
||||
|
||||
try:
|
||||
# Apply to the editor (QTextEdit or any other relevant widgets)
|
||||
self.editor.document().setDefaultStyleSheet(css)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Apply to the search widget (if it's also a rich-text widget)
|
||||
self.search.document().setDefaultStyleSheet(css)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _apply_calendar_theme(self, theme: Theme):
|
||||
"""Use orange accents on the calendar in dark mode only."""
|
||||
app_pal = QApplication.instance().palette()
|
||||
|
||||
if theme == Theme.DARK:
|
||||
orange = QColor("#FFA500")
|
||||
black = QColor(0, 0, 0)
|
||||
|
||||
# Per-widget palette: selection color inside the date grid
|
||||
pal = self.calendar.palette()
|
||||
pal.setColor(QPalette.Highlight, orange)
|
||||
pal.setColor(QPalette.HighlightedText, black)
|
||||
self.calendar.setPalette(pal)
|
||||
|
||||
# Stylesheet: nav bar + selected-day background
|
||||
self.calendar.setStyleSheet("""
|
||||
QWidget#qt_calendar_navigationbar { background-color: #FFA500; }
|
||||
QCalendarWidget QToolButton { color: black; }
|
||||
QCalendarWidget QToolButton:hover { background-color: rgba(255,165,0,0.20); }
|
||||
/* Selected day color in the table view */
|
||||
QCalendarWidget QTableView:enabled {
|
||||
selection-background-color: #FFA500;
|
||||
selection-color: black;
|
||||
}
|
||||
/* Optional: keep weekday header readable */
|
||||
QCalendarWidget QTableView QHeaderView::section {
|
||||
background: transparent;
|
||||
color: palette(windowText);
|
||||
}
|
||||
""")
|
||||
else:
|
||||
# Back to app defaults in light/system
|
||||
self.calendar.setPalette(app_pal)
|
||||
self.calendar.setStyleSheet("")
|
||||
|
||||
# Keep weekend text color in sync with the current palette
|
||||
self._apply_calendar_text_colors()
|
||||
self.calendar.update()
|
||||
|
||||
def _apply_calendar_text_colors(self):
|
||||
pal = self.palette()
|
||||
txt = pal.windowText().color()
|
||||
fmt = QTextCharFormat()
|
||||
fmt.setForeground(txt)
|
||||
# Use normal text color for weekends
|
||||
self.calendar.setWeekdayTextFormat(Qt.Saturday, fmt)
|
||||
self.calendar.setWeekdayTextFormat(Qt.Sunday, fmt)
|
||||
|
||||
def _on_search_dates_changed(self, date_strs: list[str]):
|
||||
dates = set()
|
||||
for ds in date_strs or []:
|
||||
|
|
@ -323,7 +437,16 @@ class MainWindow(QMainWindow):
|
|||
self._apply_search_highlights(dates)
|
||||
|
||||
def _apply_search_highlights(self, dates: set):
|
||||
yellow = QBrush(QColor("#fff9c4"))
|
||||
pal = self.palette()
|
||||
base = pal.base().color()
|
||||
hi = pal.highlight().color()
|
||||
# Blend highlight with base so it looks soft in both modes
|
||||
blend = QColor(
|
||||
(2 * hi.red() + base.red()) // 3,
|
||||
(2 * hi.green() + base.green()) // 3,
|
||||
(2 * hi.blue() + base.blue()) // 3,
|
||||
)
|
||||
yellow = QBrush(blend)
|
||||
old = getattr(self, "_search_highlighted_dates", set())
|
||||
|
||||
for d in old - dates: # clear removed
|
||||
|
|
@ -364,10 +487,10 @@ class MainWindow(QMainWindow):
|
|||
bf = c.blockFormat()
|
||||
|
||||
# Block signals so setChecked() doesn't re-trigger actions
|
||||
blocker1 = QSignalBlocker(self.toolBar.actBold)
|
||||
blocker2 = QSignalBlocker(self.toolBar.actItalic)
|
||||
blocker3 = QSignalBlocker(self.toolBar.actUnderline)
|
||||
blocker4 = QSignalBlocker(self.toolBar.actStrike)
|
||||
QSignalBlocker(self.toolBar.actBold)
|
||||
QSignalBlocker(self.toolBar.actItalic)
|
||||
QSignalBlocker(self.toolBar.actUnderline)
|
||||
QSignalBlocker(self.toolBar.actStrike)
|
||||
|
||||
self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
|
||||
self.toolBar.actItalic.setChecked(fmt.fontItalic())
|
||||
|
|
@ -384,10 +507,10 @@ class MainWindow(QMainWindow):
|
|||
bH2 = _approx(cur_size, 18)
|
||||
bH3 = _approx(cur_size, 14)
|
||||
|
||||
b1 = QSignalBlocker(self.toolBar.actH1)
|
||||
b2 = QSignalBlocker(self.toolBar.actH2)
|
||||
b3 = QSignalBlocker(self.toolBar.actH3)
|
||||
bN = QSignalBlocker(self.toolBar.actNormal)
|
||||
QSignalBlocker(self.toolBar.actH1)
|
||||
QSignalBlocker(self.toolBar.actH2)
|
||||
QSignalBlocker(self.toolBar.actH3)
|
||||
QSignalBlocker(self.toolBar.actNormal)
|
||||
|
||||
self.toolBar.actH1.setChecked(bH1)
|
||||
self.toolBar.actH2.setChecked(bH2)
|
||||
|
|
@ -538,6 +661,7 @@ class MainWindow(QMainWindow):
|
|||
self.cfg.path = new_cfg.path
|
||||
self.cfg.key = new_cfg.key
|
||||
self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes)
|
||||
self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme)
|
||||
|
||||
# Persist once
|
||||
save_db_config(self.cfg)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ from PySide6.QtWidgets import (
|
|||
QWidget,
|
||||
)
|
||||
|
||||
# type: rows are (date_iso, content)
|
||||
Row = Tuple[str, str]
|
||||
|
||||
|
||||
|
|
@ -102,11 +101,10 @@ class Search(QWidget):
|
|||
# Date label (plain text)
|
||||
date_lbl = QLabel()
|
||||
date_lbl.setTextFormat(Qt.TextFormat.RichText)
|
||||
date_lbl.setText(f"<i>{date_str}</i>:")
|
||||
date_lbl.setText(f"<h3><i>{date_str}</i></h3>")
|
||||
date_f = date_lbl.font()
|
||||
date_f.setPointSizeF(date_f.pointSizeF() + 1)
|
||||
date_lbl.setFont(date_f)
|
||||
date_lbl.setStyleSheet("color:#000;")
|
||||
outer.addWidget(date_lbl)
|
||||
|
||||
# Preview row with optional ellipses
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ def load_db_config() -> DBConfig:
|
|||
s = get_settings()
|
||||
path = Path(s.value("db/path", str(default_db_path())))
|
||||
key = s.value("db/key", "")
|
||||
idle = s.value("db/idle_minutes", 15, type=int)
|
||||
return DBConfig(path=path, key=key, idle_minutes=idle)
|
||||
idle = s.value("ui/idle_minutes", 15, type=int)
|
||||
theme = s.value("ui/theme", "system", type=str)
|
||||
return DBConfig(path=path, key=key, idle_minutes=idle, theme=theme)
|
||||
|
||||
|
||||
def save_db_config(cfg: DBConfig) -> None:
|
||||
s = get_settings()
|
||||
s.setValue("db/path", str(cfg.path))
|
||||
s.setValue("db/key", str(cfg.key))
|
||||
s.setValue("db/idle_minutes", str(cfg.idle_minutes))
|
||||
s.setValue("ui/idle_minutes", str(cfg.idle_minutes))
|
||||
s.setValue("ui/theme", str(cfg.theme))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from PySide6.QtWidgets import (
|
|||
QPushButton,
|
||||
QFileDialog,
|
||||
QDialogButtonBox,
|
||||
QRadioButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QMessageBox,
|
||||
|
|
@ -26,6 +27,7 @@ from PySide6.QtGui import QPalette
|
|||
|
||||
from .db import DBConfig, DBManager
|
||||
from .settings import load_db_config, save_db_config
|
||||
from .theme import Theme
|
||||
from .key_prompt import KeyPrompt
|
||||
|
||||
|
||||
|
|
@ -42,6 +44,31 @@ class SettingsDialog(QDialog):
|
|||
self.setMinimumWidth(560)
|
||||
self.setSizeGripEnabled(True)
|
||||
|
||||
current_settings = load_db_config()
|
||||
|
||||
# Add theme selection
|
||||
theme_group = QGroupBox("Theme")
|
||||
theme_layout = QVBoxLayout(theme_group)
|
||||
|
||||
self.theme_system = QRadioButton("System")
|
||||
self.theme_light = QRadioButton("Light")
|
||||
self.theme_dark = QRadioButton("Dark")
|
||||
|
||||
# Load current theme from settings
|
||||
current_theme = current_settings.theme
|
||||
if current_theme == Theme.DARK.value:
|
||||
self.theme_dark.setChecked(True)
|
||||
elif current_theme == Theme.LIGHT.value:
|
||||
self.theme_light.setChecked(True)
|
||||
else:
|
||||
self.theme_system.setChecked(True)
|
||||
|
||||
theme_layout.addWidget(self.theme_system)
|
||||
theme_layout.addWidget(self.theme_light)
|
||||
theme_layout.addWidget(self.theme_dark)
|
||||
|
||||
form.addRow(theme_group)
|
||||
|
||||
self.path_edit = QLineEdit(str(self._cfg.path))
|
||||
self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
browse_btn = QPushButton("Browse…")
|
||||
|
|
@ -64,7 +91,6 @@ class SettingsDialog(QDialog):
|
|||
|
||||
# Checkbox to remember key
|
||||
self.save_key_btn = QCheckBox("Remember key")
|
||||
current_settings = load_db_config()
|
||||
self.key = current_settings.key or ""
|
||||
self.save_key_btn.setChecked(bool(self.key))
|
||||
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
||||
|
|
@ -188,13 +214,24 @@ class SettingsDialog(QDialog):
|
|||
self.path_edit.setText(p)
|
||||
|
||||
def _save(self):
|
||||
# Save the selected theme into QSettings
|
||||
if self.theme_dark.isChecked():
|
||||
selected_theme = Theme.DARK
|
||||
elif self.theme_light.isChecked():
|
||||
selected_theme = Theme.LIGHT
|
||||
else:
|
||||
selected_theme = Theme.SYSTEM
|
||||
|
||||
key_to_save = self.key if self.save_key_btn.isChecked() else ""
|
||||
self._cfg = DBConfig(
|
||||
path=Path(self.path_edit.text()),
|
||||
key=key_to_save,
|
||||
idle_minutes=self.idle_spin.value(),
|
||||
theme=selected_theme.value,
|
||||
)
|
||||
|
||||
save_db_config(self._cfg)
|
||||
self.parent().themes.apply(selected_theme)
|
||||
self.accept()
|
||||
|
||||
def _change_key(self):
|
||||
|
|
|
|||
103
bouquin/theme.py
Normal file
103
bouquin/theme.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from PySide6.QtGui import QPalette, QColor, QGuiApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
|
||||
class Theme(Enum):
|
||||
SYSTEM = "system"
|
||||
LIGHT = "light"
|
||||
DARK = "dark"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThemeConfig:
|
||||
theme: Theme = Theme.SYSTEM
|
||||
|
||||
|
||||
class ThemeManager(QObject):
|
||||
themeChanged = Signal(Theme)
|
||||
|
||||
def __init__(self, app: QApplication, cfg: ThemeConfig):
|
||||
super().__init__()
|
||||
self._app = app
|
||||
self._cfg = cfg
|
||||
|
||||
# Follow OS if supported (Qt 6+)
|
||||
hints = QGuiApplication.styleHints()
|
||||
if hasattr(hints, "colorSchemeChanged"):
|
||||
hints.colorSchemeChanged.connect(
|
||||
lambda _: (self._cfg.theme == Theme.SYSTEM)
|
||||
and self.apply(self._cfg.theme)
|
||||
)
|
||||
|
||||
def current(self) -> Theme:
|
||||
return self._cfg.theme
|
||||
|
||||
def set(self, theme: Theme):
|
||||
self._cfg.theme = theme
|
||||
self.apply(theme)
|
||||
|
||||
def apply(self, theme: Theme):
|
||||
# Resolve "system"
|
||||
if theme == Theme.SYSTEM:
|
||||
hints = QGuiApplication.styleHints()
|
||||
scheme = getattr(hints, "colorScheme", None)
|
||||
if callable(scheme):
|
||||
scheme = hints.colorScheme()
|
||||
# 0=Light, 1=Dark in newer Qt; fall back to Light
|
||||
theme = Theme.DARK if scheme == 1 else Theme.LIGHT
|
||||
|
||||
# Always use Fusion so palette applies consistently cross-platform
|
||||
self._app.setStyle("Fusion")
|
||||
|
||||
if theme == Theme.DARK:
|
||||
pal = self._dark_palette()
|
||||
self._app.setPalette(pal)
|
||||
# keep stylesheet empty unless you need widget-specific tweaks
|
||||
self._app.setStyleSheet("")
|
||||
else:
|
||||
pal = self._light_palette()
|
||||
self._app.setPalette(pal)
|
||||
self._app.setStyleSheet("")
|
||||
|
||||
self.themeChanged.emit(theme)
|
||||
|
||||
# ----- Palettes -----
|
||||
def _dark_palette(self) -> QPalette:
|
||||
pal = QPalette()
|
||||
base = QColor(35, 35, 35)
|
||||
window = QColor(53, 53, 53)
|
||||
text = QColor(220, 220, 220)
|
||||
disabled = QColor(127, 127, 127)
|
||||
focus = QColor(42, 130, 218)
|
||||
|
||||
pal.setColor(QPalette.Window, window)
|
||||
pal.setColor(QPalette.WindowText, text)
|
||||
pal.setColor(QPalette.Base, base)
|
||||
pal.setColor(QPalette.AlternateBase, window)
|
||||
pal.setColor(QPalette.ToolTipBase, window)
|
||||
pal.setColor(QPalette.ToolTipText, text)
|
||||
pal.setColor(QPalette.Text, text)
|
||||
pal.setColor(QPalette.PlaceholderText, disabled)
|
||||
pal.setColor(QPalette.Button, window)
|
||||
pal.setColor(QPalette.ButtonText, text)
|
||||
pal.setColor(QPalette.BrightText, QColor(255, 84, 84))
|
||||
pal.setColor(QPalette.Highlight, focus)
|
||||
pal.setColor(QPalette.HighlightedText, QColor(0, 0, 0))
|
||||
pal.setColor(QPalette.Link, QColor("#FFA500"))
|
||||
pal.setColor(QPalette.LinkVisited, QColor("#B38000"))
|
||||
|
||||
return pal
|
||||
|
||||
def _light_palette(self) -> QPalette:
|
||||
# Let Qt provide its default light palette, but nudge a couple roles
|
||||
pal = self._app.style().standardPalette()
|
||||
pal.setColor(QPalette.Highlight, QColor(0, 120, 215))
|
||||
pal.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
||||
pal.setColor(
|
||||
QPalette.Link, QColor("#1a73e8")
|
||||
) # Light blue for links in light mode
|
||||
return pal
|
||||
Loading…
Add table
Add a link
Reference in a new issue