Tema clar/fosc segons el sistema i títol de finestra
This commit is contained in:
@@ -9,11 +9,14 @@ from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
from .config import load_config
|
||||
from .paths import config_file, data_root
|
||||
from .ui.main_window import MainWindow
|
||||
from .ui.theme import apply_theme, watch_system_theme
|
||||
|
||||
|
||||
def main() -> int:
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("jlauncher")
|
||||
apply_theme(app)
|
||||
watch_system_theme(app)
|
||||
|
||||
cfg_path = config_file()
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtGui import QPalette, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -52,7 +52,8 @@ class GameRow(QFrame):
|
||||
self.name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
self.desc_label = QLabel("")
|
||||
self.desc_label.setWordWrap(True)
|
||||
self.desc_label.setStyleSheet("color: #aaaaaa;")
|
||||
# Texto atenuado que sigue la paleta (claro/oscuro) en vez de un gris fijo.
|
||||
self.desc_label.setForegroundRole(QPalette.PlaceholderText)
|
||||
self.status_label = QLabel("")
|
||||
self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;")
|
||||
text_box.addWidget(self.name_label)
|
||||
|
||||
@@ -18,6 +18,9 @@ from ..config import Config, Game
|
||||
from ..workers import DownloadWorker, RunWorker
|
||||
from .game_row import GameRow
|
||||
|
||||
APP_NAME = "Jail Launcher"
|
||||
WINDOW_TITLE = f"© 2026 {APP_NAME} — JailDesigner"
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, config: Config, root: Path, parent=None) -> None:
|
||||
@@ -31,7 +34,7 @@ class MainWindow(QMainWindow):
|
||||
# llegue al hilo principal, y la UI nunca se refresca.
|
||||
self._workers: set = set()
|
||||
|
||||
self.setWindowTitle("jlauncher — Juegos jailgames")
|
||||
self.setWindowTitle(WINDOW_TITLE)
|
||||
self.resize(720, 640)
|
||||
|
||||
splitter = QSplitter(Qt.Vertical)
|
||||
@@ -58,9 +61,7 @@ class MainWindow(QMainWindow):
|
||||
self.log_view = QPlainTextEdit()
|
||||
self.log_view.setReadOnly(True)
|
||||
self.log_view.setMaximumBlockCount(5000)
|
||||
self.log_view.setStyleSheet(
|
||||
"font-family: monospace; font-size: 11px; background:#1e1e1e; color:#d4d4d4;"
|
||||
)
|
||||
self.log_view.setStyleSheet("font-family: monospace; font-size: 11px;")
|
||||
splitter.addWidget(self.log_view)
|
||||
splitter.setStretchFactor(0, 3)
|
||||
splitter.setStretchFactor(1, 1)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user