"""Ventana principal: lista de juegos con scroll + panel de log.""" from __future__ import annotations from pathlib import Path from PySide6.QtCore import QThreadPool, Qt from PySide6.QtGui import QAction from PySide6.QtWidgets import ( QMainWindow, QPlainTextEdit, QScrollArea, QSplitter, QVBoxLayout, QWidget, ) from ..config import Config, Game from ..settings import load_settings, save_settings from ..workers import CheckUpdatesWorker, 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: super().__init__(parent) self.config = config self.root = root self.settings = load_settings() self.pool = QThreadPool.globalInstance() self.rows: dict[str, GameRow] = {} # Mantener referencias a los workers en vuelo: si no, Python los recolecta # (junto a su objeto de señales) antes de que la señal en cola `finished` # llegue al hilo principal, y la UI nunca se refresca. self._workers: set = set() self.setWindowTitle(WINDOW_TITLE) self.resize(720, 640) self._build_menu() splitter = QSplitter(Qt.Vertical) # --- Lista de juegos con scroll --- list_container = QWidget() list_layout = QVBoxLayout(list_container) list_layout.setContentsMargins(6, 6, 6, 6) list_layout.setSpacing(6) for game in config.games: row = GameRow(game, root) row.download_requested.connect(self._on_download) row.run_requested.connect(self._on_run) self.rows[game.id] = row list_layout.addWidget(row) list_layout.addStretch(1) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setWidget(list_container) splitter.addWidget(scroll) # --- Panel de log --- self.log_view = QPlainTextEdit() self.log_view.setReadOnly(True) self.log_view.setMaximumBlockCount(5000) self.log_view.setStyleSheet("font-family: monospace; font-size: 11px;") splitter.addWidget(self.log_view) splitter.setStretchFactor(0, 3) splitter.setStretchFactor(1, 1) self.setCentralWidget(splitter) # Estado persistido: marcas de update + filtro de ocultar no descargados. for game_id in self.settings.updates_pending: if game_id in self.rows: self.rows[game_id].set_update_available(True) self._apply_filter() # --------------------------------------------------------------- menú def _build_menu(self) -> None: menu = self.menuBar().addMenu("Opciones") self.action_hide = QAction("Ocultar juegos no descargados", self, checkable=True) self.action_hide.setChecked(self.settings.hide_not_downloaded) self.action_hide.toggled.connect(self._on_toggle_hide) menu.addAction(self.action_hide) self.action_check = QAction("Comprobar actualizaciones", self) self.action_check.triggered.connect(self._check_updates) menu.addAction(self.action_check) def _on_toggle_hide(self, checked: bool) -> None: self.settings.hide_not_downloaded = checked save_settings(self.settings) self._apply_filter() def _apply_filter(self) -> None: hide = self.action_hide.isChecked() for row in self.rows.values(): row.setVisible(not (hide and not row.is_installed())) # ------------------------------------------------------ comprobar updates def _check_updates(self) -> None: self.action_check.setEnabled(False) self._log("=== Comprobando actualizaciones ===") worker = CheckUpdatesWorker(self.root, self.config.games) worker.signals.log.connect(self._log) worker.signals.result.connect(self._mark_update) worker.signals.finished.connect(self._check_done) worker.signals.error.connect(self._check_error) self._track(worker) self.pool.start(worker) def _mark_update(self, game_id: str, has_update: bool) -> None: row = self.rows.get(game_id) if row is not None: row.set_update_available(has_update) pending = set(self.settings.updates_pending) pending.add(game_id) if has_update else pending.discard(game_id) self.settings.updates_pending = sorted(pending) save_settings(self.settings) def _check_done(self, _payload) -> None: self.action_check.setEnabled(True) self._log("=== Comprobación de actualizaciones terminada ===") def _check_error(self, msg: str) -> None: self.action_check.setEnabled(True) self._log(f"!!! Error comprobando actualizaciones: {msg}") # --------------------------------------------------------------- helpers def _log(self, text: str) -> None: self.log_view.appendPlainText(text) def _track(self, worker) -> None: """Retiene el worker hasta que emite finished/error, evitando que el GC se lleve su objeto de señales antes de entregar la señal en cola.""" worker.setAutoDelete(False) self._workers.add(worker) worker.signals.finished.connect(lambda *_: self._workers.discard(worker)) worker.signals.error.connect(lambda *_: self._workers.discard(worker)) # --------------------------------------------------------------- acciones def _on_download(self, game: Game) -> None: row = self.rows[game.id] row.set_busy(True, "Descargando…") self._log(f"=== Download: {game.name} ===") worker = DownloadWorker(self.root, game) worker.signals.log.connect(self._log) worker.signals.finished.connect(lambda _meta, g=game: self._download_done(g)) worker.signals.error.connect(lambda msg, g=game: self._op_error(g, msg)) self._track(worker) self.pool.start(worker) def _download_done(self, game: Game) -> None: self._log(f"=== {game.name}: descarga completada ===") row = self.rows[game.id] row.set_busy(False) row.set_update_available(False) # recién traído del remoto → al día if game.id in self.settings.updates_pending: self.settings.updates_pending = [ g for g in self.settings.updates_pending if g != game.id ] save_settings(self.settings) self._apply_filter() # un juego antes no instalado puede aparecer ahora def _on_run(self, game: Game) -> None: row = self.rows[game.id] row.set_busy(True, "Ejecutando…") self._log(f"=== Run: {game.name} ===") worker = RunWorker(self.root, game) worker.signals.log.connect(self._log) worker.signals.finished.connect(lambda code, g=game: self._run_done(g, code)) worker.signals.error.connect(lambda msg, g=game: self._op_error(g, msg)) self._track(worker) self.pool.start(worker) def _run_done(self, game: Game, code: int) -> None: self._log(f"=== {game.name}: finalizó con código {code} ===") row = self.rows[game.id] row.set_busy(False) row.refresh() def _op_error(self, game: Game, msg: str) -> None: self._log(f"!!! {game.name}: ERROR: {msg}") row = self.rows[game.id] row.set_busy(False) row.refresh()