121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""Tema claro/oscuro siguiendo el esquema de color del sistema.
|
|
|
|
Usa el estilo Fusion (consistente entre plataformas) y aplica una paleta clara u
|
|
oscura según ``QStyleHints.colorScheme()``. Los widgets que no fuerzan colores
|
|
propios (consola de log, fondos) siguen automáticamente esta paleta.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QColor, QPalette
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
|
|
def _portal_is_dark() -> bool | None:
|
|
"""Consulta el portal XDG (org.freedesktop.appearance color-scheme).
|
|
|
|
Devuelve True (1=dark), False (2=light) o None (0=sin preferencia / no disponible).
|
|
Es la fuente más fiable en Linux cuando Qt no detecta el esquema.
|
|
"""
|
|
try:
|
|
out = subprocess.run(
|
|
[
|
|
"gdbus", "call", "--session",
|
|
"--dest", "org.freedesktop.portal.Desktop",
|
|
"--object-path", "/org/freedesktop/portal/desktop",
|
|
"--method", "org.freedesktop.portal.Settings.Read",
|
|
"org.freedesktop.appearance", "color-scheme",
|
|
],
|
|
capture_output=True, text=True, timeout=2,
|
|
)
|
|
except (OSError, subprocess.SubprocessError):
|
|
return None
|
|
if out.returncode != 0:
|
|
return None
|
|
if "uint32 1" in out.stdout:
|
|
return True
|
|
if "uint32 2" in out.stdout:
|
|
return False
|
|
return None
|
|
|
|
|
|
def _gsettings_is_dark() -> bool | None:
|
|
"""Respaldo: gsettings color-scheme de GNOME ('prefer-dark')."""
|
|
try:
|
|
out = subprocess.run(
|
|
["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"],
|
|
capture_output=True, text=True, timeout=2,
|
|
)
|
|
except (OSError, subprocess.SubprocessError):
|
|
return None
|
|
if out.returncode != 0:
|
|
return None
|
|
return "dark" in out.stdout.lower()
|
|
|
|
|
|
def system_is_dark(app: QApplication) -> bool:
|
|
"""True si el sistema está en modo oscuro.
|
|
|
|
Prioriza el aviso de Qt; si Qt dice claro (o no soporta la API), consulta el
|
|
portal XDG y gsettings, que en sesiones xcb suelen ser más fiables que Qt.
|
|
"""
|
|
hints = app.styleHints()
|
|
if hasattr(hints, "colorScheme"):
|
|
try:
|
|
if hints.colorScheme() == Qt.ColorScheme.Dark:
|
|
return True
|
|
except Exception: # noqa: BLE001 - APIs viejas/raras: caer al fallback
|
|
pass
|
|
for probe in (_portal_is_dark, _gsettings_is_dark):
|
|
val = probe()
|
|
if val is not None:
|
|
return val
|
|
return app.palette().color(QPalette.Window).lightness() < 128
|
|
|
|
|
|
def _dark_palette() -> QPalette:
|
|
p = QPalette()
|
|
window = QColor(0x2b, 0x2b, 0x2b)
|
|
base = QColor(0x1e, 0x1e, 0x1e)
|
|
alt = QColor(0x35, 0x35, 0x35)
|
|
text = QColor(0xdc, 0xdc, 0xdc)
|
|
disabled = QColor(0x7f, 0x7f, 0x7f)
|
|
highlight = QColor(0x2a, 0x82, 0xda)
|
|
|
|
p.setColor(QPalette.Window, window)
|
|
p.setColor(QPalette.WindowText, text)
|
|
p.setColor(QPalette.Base, base)
|
|
p.setColor(QPalette.AlternateBase, alt)
|
|
p.setColor(QPalette.ToolTipBase, window)
|
|
p.setColor(QPalette.ToolTipText, text)
|
|
p.setColor(QPalette.Text, text)
|
|
p.setColor(QPalette.Button, alt)
|
|
p.setColor(QPalette.ButtonText, text)
|
|
p.setColor(QPalette.BrightText, Qt.red)
|
|
p.setColor(QPalette.Link, highlight)
|
|
p.setColor(QPalette.Highlight, highlight)
|
|
p.setColor(QPalette.HighlightedText, Qt.black)
|
|
p.setColor(QPalette.PlaceholderText, disabled)
|
|
for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText):
|
|
p.setColor(QPalette.Disabled, role, disabled)
|
|
return p
|
|
|
|
|
|
def apply_theme(app: QApplication) -> None:
|
|
"""Aplica estilo Fusion + paleta acorde al esquema del sistema."""
|
|
app.setStyle("Fusion")
|
|
if system_is_dark(app):
|
|
app.setPalette(_dark_palette())
|
|
else:
|
|
app.setPalette(app.style().standardPalette())
|
|
|
|
|
|
def watch_system_theme(app: QApplication) -> None:
|
|
"""Re-aplica el tema cuando el sistema cambia entre claro/oscuro en caliente."""
|
|
hints = app.styleHints()
|
|
if hasattr(hints, "colorSchemeChanged"):
|
|
hints.colorSchemeChanged.connect(lambda _scheme: apply_theme(app))
|