107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""Theme and window helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import platform
|
|
from tkinter import ttk
|
|
|
|
try:
|
|
import winreg
|
|
except Exception: # pragma: no cover - platform-specific
|
|
winreg = None # type: ignore
|
|
|
|
|
|
class ThemeMixin:
|
|
"""Provides theme handling utilities for the main application."""
|
|
|
|
theme: str
|
|
style: ttk.Style
|
|
scale_style: str
|
|
|
|
def init_theme(self) -> None:
|
|
"""Initialise ttk style handling and apply the detected theme."""
|
|
self.style = ttk.Style()
|
|
self.style.theme_use("clam")
|
|
|
|
self.theme = "light"
|
|
self.apply_theme(self.detect_system_theme())
|
|
|
|
def apply_theme(self, mode: str) -> None:
|
|
"""Apply light/dark theme including widget palette."""
|
|
mode = (mode or "light").lower()
|
|
self.theme = "dark" if mode == "dark" else "light"
|
|
|
|
self.scale_style = "Horizontal.TScale"
|
|
|
|
if self.theme == "dark":
|
|
bg, fg = "#0f0f10", "#f1f1f1"
|
|
status_fg = "#f5f5f5"
|
|
highlight_fg = "#f2c744"
|
|
else:
|
|
bg, fg = "#ffffff", "#202020"
|
|
status_fg = "#1c1c1c"
|
|
highlight_fg = "#c56217"
|
|
self.root.configure(bg=bg) # type: ignore[attr-defined]
|
|
|
|
s = self.style
|
|
s.configure("TFrame", background=bg)
|
|
s.configure("TLabel", background=bg, foreground=fg, font=("Segoe UI", 10))
|
|
s.configure(
|
|
"TButton", padding=8, relief="flat", background="#e0e0e0", foreground=fg, font=("Segoe UI", 10)
|
|
)
|
|
s.map("TButton", background=[("active", "#d0d0d0")])
|
|
|
|
button_refresher = getattr(self, "_refresh_toolbar_buttons_theme", None)
|
|
if callable(button_refresher):
|
|
button_refresher()
|
|
|
|
nav_refresher = getattr(self, "_refresh_navigation_buttons_theme", None)
|
|
if callable(nav_refresher):
|
|
nav_refresher()
|
|
|
|
status_refresher = getattr(self, "_refresh_status_palette", None)
|
|
if callable(status_refresher) and hasattr(self, "status"):
|
|
status_refresher(status_fg)
|
|
|
|
accent_refresher = getattr(self, "_refresh_accent_labels", None)
|
|
if callable(accent_refresher) and hasattr(self, "filename_label"):
|
|
accent_refresher(highlight_fg)
|
|
|
|
canvas_refresher = getattr(self, "_refresh_canvas_backgrounds", None)
|
|
if callable(canvas_refresher):
|
|
canvas_refresher()
|
|
|
|
def detect_system_theme(self) -> str:
|
|
"""Best-effort detection of the OS theme preference."""
|
|
try:
|
|
if platform.system() == "Windows" and winreg is not None:
|
|
key = winreg.OpenKey(
|
|
winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
|
)
|
|
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
|
return "light" if int(value) == 1 else "dark"
|
|
except Exception:
|
|
pass
|
|
return "light"
|
|
|
|
def bring_to_front(self) -> None:
|
|
"""Try to focus the window and raise it to the foreground."""
|
|
try:
|
|
self.root.lift()
|
|
self.root.focus_force()
|
|
self.root.attributes("-topmost", True)
|
|
self.root.update()
|
|
self.root.attributes("-topmost", False)
|
|
except Exception:
|
|
pass
|
|
|
|
def toggle_theme(self) -> None:
|
|
"""Toggle between light and dark themes."""
|
|
next_mode = "dark" if self.theme == "light" else "light"
|
|
self.apply_theme(next_mode)
|
|
self.update_preview() # type: ignore[attr-defined]
|
|
|
|
|
|
__all__ = ["ThemeMixin"]
|