diff --git a/jlauncher/__main__.py b/jlauncher/__main__.py index a441d5c..606d919 100644 --- a/jlauncher/__main__.py +++ b/jlauncher/__main__.py @@ -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: diff --git a/jlauncher/ui/game_row.py b/jlauncher/ui/game_row.py index b3e14a3..7fd4e41 100644 --- a/jlauncher/ui/game_row.py +++ b/jlauncher/ui/game_row.py @@ -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) diff --git a/jlauncher/ui/main_window.py b/jlauncher/ui/main_window.py index 58d06fd..1811785 100644 --- a/jlauncher/ui/main_window.py +++ b/jlauncher/ui/main_window.py @@ -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) diff --git a/jlauncher/ui/theme.py b/jlauncher/ui/theme.py new file mode 100644 index 0000000..26cdbb3 --- /dev/null +++ b/jlauncher/ui/theme.py @@ -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))