"""Theme and window helpers.""" from __future__ import annotations import platform from tkinter import ttk try: import ttkbootstrap as tb # type: ignore HAS_TTKBOOTSTRAP = True except Exception: # pragma: no cover - optional dependency tb = None HAS_TTKBOOTSTRAP = False 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 using_tb: bool scale_style: str def init_theme(self) -> None: """Initialise ttk style handling and apply the detected theme.""" if HAS_TTKBOOTSTRAP: try: try: self.root.tk.call("package", "require", "msgcat") # type: ignore[attr-defined] except Exception: pass self.style = tb.Style() self.using_tb = True except Exception: self.style = ttk.Style() self.style.theme_use("clam") self.using_tb = False else: self.style = ttk.Style() self.style.theme_use("clam") self.using_tb = False 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" if HAS_TTKBOOTSTRAP: try: theme_name = "darkly" if self.theme == "dark" else "flatly" self.style.theme_use(theme_name) except Exception: pass self.scale_style = ( "info.Horizontal.TScale" if self.theme == "dark" else "primary.Horizontal.TScale" ) else: self.scale_style = "Horizontal.TScale" if self.theme == "dark": bg, fg = "#0f0f10", "#f1f1f1" status_fg = "#f5f5f5" else: bg, fg = "#ededf2", "#202020" status_fg = "#1c1c1c" 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)) if not HAS_TTKBOOTSTRAP: 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() status_refresher = getattr(self, "_refresh_status_palette", None) if callable(status_refresher) and hasattr(self, "status"): status_refresher(status_fg) 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", "HAS_TTKBOOTSTRAP"]