Move lock_overlay/calendar theming to the ThemeManager

This commit is contained in:
Miguel Jacq 2025-11-12 10:51:08 +11:00
parent 118e192639
commit 494b14136b
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
5 changed files with 154 additions and 157 deletions

View file

@ -1,12 +1,13 @@
from __future__ import annotations
from PySide6.QtCore import Qt, QEvent
from PySide6.QtGui import QPalette
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
from .theme import ThemeManager
class LockOverlay(QWidget):
def __init__(self, parent: QWidget, on_unlock: callable):
def __init__(self, parent: QWidget, on_unlock: callable, themes: ThemeManager):
"""
Widget that 'locks' the screen after a configured idle time.
"""
@ -16,7 +17,6 @@ class LockOverlay(QWidget):
self.setFocusPolicy(Qt.StrongFocus)
self.setGeometry(parent.rect())
self._styling = False # <-- reentrancy guard
self._last_dark: bool | None = None
lay = QVBoxLayout(self)
@ -38,91 +38,9 @@ class LockOverlay(QWidget):
lay.addWidget(self._btn, 0, Qt.AlignCenter)
lay.addStretch(1)
self._apply_overlay_style()
themes.register_lock_overlay(self)
self.hide()
def _is_dark(self, pal: QPalette) -> bool:
"""
Detect if dark mode is in use.
"""
c = pal.color(QPalette.Window)
luma = 0.2126 * c.redF() + 0.7152 * c.greenF() + 0.0722 * c.blueF()
return luma < 0.5
def _apply_overlay_style(self):
if self._styling:
return
dark = self._is_dark(self.palette())
if dark == self._last_dark:
return
self._styling = True
try:
if dark:
link = self.palette().color(QPalette.Link)
accent_hex = link.name() # e.g. "#FFA500"
r, g, b = link.red(), link.green(), link.blue()
self.setStyleSheet(
f"""
#LockOverlay {{ background-color: rgb(0,0,0); }}
#LockOverlay QLabel#lockLabel {{ color: {accent_hex}; font-weight: 600; }}
#LockOverlay QPushButton#unlockButton {{
color: {accent_hex};
background-color: rgba({r},{g},{b},0.10);
border: 1px solid {accent_hex};
border-radius: 8px;
padding: 8px 16px;
}}
#LockOverlay QPushButton#unlockButton:hover {{
background-color: rgba({r},{g},{b},0.16);
border-color: {accent_hex};
}}
#LockOverlay QPushButton#unlockButton:pressed {{
background-color: rgba({r},{g},{b},0.24);
}}
#LockOverlay QPushButton#unlockButton:focus {{
outline: none;
border-color: {accent_hex};
}}
"""
)
else:
# (light mode unchanged)
self.setStyleSheet(
"""
#LockOverlay { background-color: rgba(0,0,0,120); }
#LockOverlay QLabel#lockLabel { color: palette(window-text); font-weight: 600; }
#LockOverlay QPushButton#unlockButton {
color: palette(button-text);
background-color: rgba(255,255,255,0.92);
border: 1px solid rgba(0,0,0,0.25);
border-radius: 8px;
padding: 8px 16px;
}
#LockOverlay QPushButton#unlockButton:hover {
background-color: rgba(255,255,255,1.0);
border-color: rgba(0,0,0,0.35);
}
#LockOverlay QPushButton#unlockButton:pressed {
background-color: rgba(245,245,245,1.0);
}
#LockOverlay QPushButton#unlockButton:focus {
outline: none;
border-color: palette(highlight);
}
"""
)
self._last_dark = dark
finally:
self._styling = False
def changeEvent(self, ev):
super().changeEvent(ev)
# Only re-style on palette flips (user changed theme)
if ev.type() in (QEvent.PaletteChange, QEvent.ApplicationPaletteChange):
self._apply_overlay_style()
def eventFilter(self, obj, event):
if obj is self.parent() and event.type() in (QEvent.Resize, QEvent.Show):
self.setGeometry(obj.rect())

View file

@ -25,13 +25,11 @@ from PySide6.QtGui import (
QFont,
QGuiApplication,
QKeySequence,
QPalette,
QTextCharFormat,
QTextCursor,
QTextListFormat,
)
from PySide6.QtWidgets import (
QApplication,
QCalendarWidget,
QDialog,
QFileDialog,
@ -57,7 +55,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
from .theme import ThemeManager
class MainWindow(QMainWindow):
@ -87,6 +85,7 @@ class MainWindow(QMainWindow):
self.calendar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.calendar.setGridVisible(True)
self.calendar.selectionChanged.connect(self._on_date_changed)
self.themes.register_calendar(self.calendar)
self.search = Search(self.db)
self.search.openDateRequested.connect(self._load_selected_date)
@ -147,7 +146,9 @@ class MainWindow(QMainWindow):
self._idle_timer.start()
# full-window overlay that sits on top of the central widget
self._lock_overlay = LockOverlay(self.centralWidget(), self._on_unlock_clicked)
self._lock_overlay = LockOverlay(
self.centralWidget(), self._on_unlock_clicked, themes=self.themes
)
self.centralWidget().installEventFilter(self._lock_overlay)
self._locked = False
@ -278,12 +279,9 @@ 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()
@ -812,69 +810,11 @@ class MainWindow(QMainWindow):
# ----------------- Some theme helpers -------------------#
def _retheme_overrides(self):
if hasattr(self, "_lock_overlay"):
self._lock_overlay._apply_overlay_style()
self._apply_calendar_text_colors()
self._apply_link_css()
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 = Theme.ORANGE_ANCHOR.value
visited = Theme.ORANGE_ANCHOR_VISITED.value
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)
self.editor.document().setDefaultStyleSheet(css)
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:
highlight = QColor(Theme.ORANGE_ANCHOR.value)
black = QColor(0, 0, 0)
highlight_css = Theme.ORANGE_ANCHOR.value
# Per-widget palette: selection color inside the date grid
pal = self.calendar.palette()
pal.setColor(QPalette.Highlight, highlight)
pal.setColor(QPalette.HighlightedText, black)
self.calendar.setPalette(pal)
# Stylesheet: nav bar + selected-day background
self.calendar.setStyleSheet(
f"""
QWidget#qt_calendar_navigationbar {{ background-color: {highlight_css}; }}
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: {highlight_css};
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("")
self._apply_calendar_text_colors()
self.calendar.update()
def _apply_calendar_text_colors(self):
pal = self.palette()
txt = pal.windowText().color()

View file

@ -62,7 +62,10 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self.code_block_format.setFontFixedPitch(True)
pal = QGuiApplication.palette()
if self.theme_manager.current() == Theme.DARK:
if (
self.theme_manager.current() == Theme.DARK
or self.theme_manager._is_system_dark
):
# In dark mode, use a darker panel-like background
bg = pal.color(QPalette.AlternateBase)
fg = pal.color(QPalette.Text)

View file

@ -2,8 +2,9 @@ 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.QtWidgets import QApplication, QCalendarWidget, QWidget
from PySide6.QtCore import QObject, Signal
from weakref import WeakSet
class Theme(Enum):
@ -26,6 +27,9 @@ class ThemeManager(QObject):
super().__init__()
self._app = app
self._cfg = cfg
self._current = None
self._calendars: "WeakSet[QCalendarWidget]" = WeakSet()
self._lock_overlays: "WeakSet[QWidget]" = WeakSet()
# Follow OS if supported (Qt 6+)
hints = QGuiApplication.styleHints()
@ -40,6 +44,15 @@ class ThemeManager(QObject):
# Heuristic: dark windows/backgrounds mean dark system theme
return pal.color(QPalette.Window).lightness() < 128
def _restyle_registered(self) -> None:
for cal in list(self._calendars):
if cal is not None:
self._apply_calendar_theme(cal)
for overlay in list(self._lock_overlays):
if overlay is not None:
self._apply_lock_overlay_theme(overlay)
def current(self) -> Theme:
return self._cfg.theme
@ -63,7 +76,19 @@ class ThemeManager(QObject):
self._app.setPalette(pal)
self._current = resolved
self.themeChanged.emit(theme)
# Re-style any registered widgets
self._restyle_registered()
self.themeChanged.emit(self._current)
def register_calendar(self, cal: QCalendarWidget) -> None:
"""Start theming calendar and keep it in sync with theme changes."""
self._calendars.add(cal)
self._apply_calendar_theme(cal)
def register_lock_overlay(self, overlay: QWidget) -> None:
"""Start theming lock overlay and keep it in sync with theme changes."""
self._lock_overlays.add(overlay)
self._apply_lock_overlay_theme(overlay)
# ----- Palettes -----
def _dark_palette(self) -> QPalette:
@ -123,3 +148,113 @@ class ThemeManager(QObject):
pal.setColor(QPalette.LinkVisited, QColor("#6b4ca5"))
return pal
def _apply_calendar_theme(self, cal: QCalendarWidget) -> None:
"""Use orange accents on the calendar in dark mode only."""
app_pal = QApplication.instance().palette()
is_dark = (self.current() == Theme.DARK) or (
self.current() == Theme.SYSTEM and self._is_system_dark()
)
if is_dark:
highlight_css = Theme.ORANGE_ANCHOR.value
highlight = QColor(highlight_css)
black = QColor(0, 0, 0)
# Per-widget palette: selection color inside the date grid
pal = cal.palette()
pal.setColor(QPalette.Highlight, highlight)
pal.setColor(QPalette.HighlightedText, black)
cal.setPalette(pal)
# Stylesheet: nav bar + selected-day background
cal.setStyleSheet(self._calendar_qss(highlight_css))
else:
# Back to app defaults in light/system-light
cal.setPalette(app_pal)
cal.setStyleSheet("")
cal.update()
def _calendar_qss(self, highlight_css: str) -> str:
return f"""
QWidget#qt_calendar_navigationbar {{ background-color: {highlight_css}; }}
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: {highlight_css};
selection-color: black;
}}
/* Keep weekday header readable */
QCalendarWidget QTableView QHeaderView::section {{
background: transparent;
color: palette(windowText);
}}
"""
def _apply_lock_overlay_theme(self, overlay: QWidget) -> None:
"""
Style the LockOverlay (objectName 'LockOverlay') using theme colors.
Dark: opaque black bg, orange accent; Light: translucent scrim, palette-driven colors.
"""
pal = QApplication.instance().palette()
is_dark = (self.current() == Theme.DARK) or (
self.current() == Theme.SYSTEM and self._is_system_dark()
)
if is_dark:
# Use the link color as the accent (you set this to ORANGE in dark palette)
accent = pal.color(QPalette.Link)
r, g, b = accent.red(), accent.green(), accent.blue()
accent_hex = accent.name()
qss = f"""
#LockOverlay {{ background-color: rgb(0,0,0); }}
#LockOverlay QLabel#lockLabel {{ color: {accent_hex}; font-weight: 600; }}
#LockOverlay QPushButton#unlockButton {{
color: {accent_hex};
background-color: rgba({r},{g},{b},0.10);
border: 1px solid {accent_hex};
border-radius: 8px;
padding: 8px 16px;
}}
#LockOverlay QPushButton#unlockButton:hover {{
background-color: rgba({r},{g},{b},0.16);
border-color: {accent_hex};
}}
#LockOverlay QPushButton#unlockButton:pressed {{
background-color: rgba({r},{g},{b},0.24);
}}
#LockOverlay QPushButton#unlockButton:focus {{
outline: none;
border-color: {accent_hex};
}}
"""
else:
qss = """
#LockOverlay { background-color: rgba(0,0,0,120); }
#LockOverlay QLabel#lockLabel { color: palette(window-text); font-weight: 600; }
#LockOverlay QPushButton#unlockButton {
color: palette(button-text);
background-color: rgba(255,255,255,0.92);
border: 1px solid rgba(0,0,0,0.25);
border-radius: 8px;
padding: 8px 16px;
}
#LockOverlay QPushButton#unlockButton:hover {
background-color: rgba(255,255,255,1.0);
border-color: rgba(0,0,0,0.35);
}
#LockOverlay QPushButton#unlockButton:pressed {
background-color: rgba(245,245,245,1.0);
}
#LockOverlay QPushButton#unlockButton:focus {
outline: none;
border-color: palette(highlight);
}
"""
overlay.setStyleSheet(qss)
overlay.update()

View file

@ -2,15 +2,16 @@ import pytest
from PySide6.QtCore import QEvent
from PySide6.QtWidgets import QWidget
from bouquin.lock_overlay import LockOverlay
from bouquin.theme import ThemeManager, ThemeConfig, Theme
@pytest.mark.gui
def test_lock_overlay_reacts_to_theme(qtbot):
def test_lock_overlay_reacts_to_theme(app, qtbot):
host = QWidget()
qtbot.addWidget(host)
host.show()
ol = LockOverlay(host, on_unlock=lambda: None)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
ol = LockOverlay(host, on_unlock=lambda: None, themes=themes)
qtbot.addWidget(ol)
ol.show()