"""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.QtWidgets import ( QMainWindow, QPlainTextEdit, QScrollArea, QSplitter, QVBoxLayout, QWidget, ) from ..config import Config, Game from ..workers import DownloadWorker, RunWorker from .game_row import GameRow class MainWindow(QMainWindow): def __init__(self, config: Config, root: Path, parent=None) -> None: super().__init__(parent) self.config = config self.root = root 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("jlauncher — Juegos jailgames") self.resize(720, 640) 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; background:#1e1e1e; color:#d4d4d4;" ) splitter.addWidget(self.log_view) splitter.setStretchFactor(0, 3) splitter.setStretchFactor(1, 1) self.setCentralWidget(splitter) # --------------------------------------------------------------- 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.refresh() 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()