diff --git a/jlauncher/ui/game_row.py b/jlauncher/ui/game_row.py index f87a7c6..8a1c7bf 100644 --- a/jlauncher/ui/game_row.py +++ b/jlauncher/ui/game_row.py @@ -1,17 +1,23 @@ -"""Fila de la lista: icono, nombre, descripción y botones Download/Run.""" +"""Fila de la llista: la fila sencera actua com un botó. + +Un clic fa l'acció principal segons l'estat: + - no descarregat → descarrega + - update pendent → actualitza + - descarregat i al dia → juga +Un indicador d'icona a la dreta (passiu) mostra què farà el clic. L'esborrat es +gestiona per fora (mètode a definir); aquest widget només exposa la senyal. +""" from __future__ import annotations from pathlib import Path -from PySide6.QtCore import QSize, Qt, Signal +from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QPalette, QPixmap from PySide6.QtWidgets import ( QFrame, QHBoxLayout, QLabel, - QPushButton, - QStyle, QVBoxLayout, ) @@ -23,38 +29,43 @@ ICON_SIZE = 64 class GameRow(QFrame): - """Widget de una fila. Emite señales cuando se pulsan los botones.""" + """Fila clicable. Emet `activated` en clic i `delete_requested` per al futur esborrat.""" - download_requested = Signal(object) # Game - run_requested = Signal(object) # Game - delete_requested = Signal(object) # Game + activated = Signal(object) # Game — clic sobre la fila (acció principal) + delete_requested = Signal(object) # Game — esborrar la descàrrega local def __init__(self, game: Game, root: Path, parent=None) -> None: super().__init__(parent) self.game = game self.root = root self._update_available = False + self._busy = False self.setObjectName("gameRow") self.setFrameShape(QFrame.StyledPanel) + # La fila és un botó: cursor de mà i ressaltat en passar el ratolí. + self.setCursor(Qt.PointingHandCursor) + self.setStyleSheet( + "QFrame#gameRow:hover { background: rgba(127, 127, 127, 0.15); }" + ) layout = QHBoxLayout(self) layout.setContentsMargins(10, 8, 10, 8) layout.setSpacing(12) - # --- Icono --- + # --- Icona del joc --- self.icon_label = QLabel() self.icon_label.setFixedSize(ICON_SIZE, ICON_SIZE) self.icon_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.icon_label) - # --- Texto (nombre + descripción + estado) --- + # --- Text (nom + descripció + estat) --- text_box = QVBoxLayout() text_box.setSpacing(2) self.name_label = QLabel(game.name) self.name_label.setStyleSheet("font-weight: bold; font-size: 14px;") self.desc_label = QLabel("") self.desc_label.setWordWrap(True) - # Texto atenuado que sigue la paleta (claro/oscuro) en vez de un gris fijo. + # Text atenuat que segueix la paleta (clar/fosc) en lloc d'un gris fix. self.desc_label.setForegroundRole(QPalette.PlaceholderText) self.status_label = QLabel("") self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;") @@ -64,47 +75,51 @@ class GameRow(QFrame): text_box.addStretch(1) layout.addLayout(text_box, stretch=1) - # --- Botons (icones compactes amb tooltip; visibilitat segons l'estat) --- - style = self.style() - self._icon_download = style.standardIcon(QStyle.SP_ArrowDown) - self._icon_update = style.standardIcon(QStyle.SP_BrowserReload) + # --- Indicador d'acció (passiu, text): què farà el clic --- + self.action_label = QLabel() + self.action_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.action_label.setMinimumWidth(96) + layout.addWidget(self.action_label) - self.download_btn = QPushButton() - self.run_btn = QPushButton() - self.run_btn.setIcon(style.standardIcon(QStyle.SP_MediaPlay)) - self.run_btn.setToolTip("Juga") - self.delete_btn = QPushButton() - self.delete_btn.setIcon(style.standardIcon(QStyle.SP_TrashIcon)) - self.delete_btn.setToolTip("Esborra") - for btn in (self.download_btn, self.run_btn, self.delete_btn): - btn.setFixedSize(36, 30) - btn.setIconSize(QSize(18, 18)) - self.download_btn.clicked.connect(lambda: self.download_requested.emit(self.game)) - self.run_btn.clicked.connect(lambda: self.run_requested.emit(self.game)) - self.delete_btn.clicked.connect(lambda: self.delete_requested.emit(self.game)) - - btn_box = QHBoxLayout() - btn_box.setSpacing(4) - btn_box.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - btn_box.addWidget(self.download_btn) - btn_box.addWidget(self.run_btn) - btn_box.addWidget(self.delete_btn) - layout.addLayout(btn_box) + # Els fills no intercepten el ratolí: tot clic arriba a la fila. + for child in ( + self.icon_label, + self.name_label, + self.desc_label, + self.status_label, + self.action_label, + ): + child.setAttribute(Qt.WA_TransparentForMouseEvents) self.refresh() - # ----------------------------------------------------------------- estado + # ------------------------------------------------------------- clic fila + + def mouseReleaseEvent(self, event) -> None: + if ( + event.button() == Qt.LeftButton + and not self._busy + and self.rect().contains(event.position().toPoint()) + ): + self.activated.emit(self.game) + super().mouseReleaseEvent(event) + + # ----------------------------------------------------------------- estat def is_installed(self) -> bool: return (repo_dir(self.root, self.game.id) / ".git").exists() + def primary_action_is_download(self) -> bool: + """True si el clic ha de descarregar/actualitzar; False si ha de jugar.""" + return (not self.is_installed()) or self._update_available + def set_update_available(self, available: bool) -> None: - """Marca/desmarca la fila como 'tiene actualización pendiente'.""" + """Marca/desmarca la fila com a 'té actualització pendent'.""" self._update_available = available self.refresh() def refresh(self) -> None: - """Recarga icono, descripción y estado desde la cache local.""" + """Recarrega icona, descripció i estat des de la cache local.""" meta = load_meta(self.root, self.game.id) self._set_icon() self.desc_label.setText(meta.description or "(sense descripció encara)") @@ -128,40 +143,32 @@ class GameRow(QFrame): ) def _set_status(self, meta: GameMeta) -> None: - installed = self.is_installed() - - # Visibilitat dels botons segons l'estat: - # - Juga / Esborra: només si està descarregat. - # - Descarrega: si no està descarregat o hi ha update pendent. - self.run_btn.setVisible(installed) - self.delete_btn.setVisible(installed) - self.download_btn.setVisible((not installed) or self._update_available) - if installed and self._update_available: - self.download_btn.setIcon(self._icon_update) - self.download_btn.setToolTip("Actualitza") - else: - self.download_btn.setIcon(self._icon_download) - self.download_btn.setToolTip("Descarrega") - - if not installed: + if not self.is_installed(): self.status_label.setText("No descarregat") self.status_label.setStyleSheet("color: #cc8855; font-size: 11px;") + self._set_action("Descarrega", "#4a90d9") elif self._update_available: self.status_label.setText("⬆ Actualització disponible") self.status_label.setStyleSheet( "color: #e0a030; font-weight: bold; font-size: 11px;" ) + self._set_action("Actualitza", "#e0a030") else: ver = f" {meta.version}" if meta.version else "" self.status_label.setText(f"Descarregat{ver}") self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;") + self._set_action("Juga", "#6fae6f") + + def _set_action(self, text: str, color: str) -> None: + self.action_label.setText(text) + self.action_label.setStyleSheet( + f"color: {color}; font-weight: bold; font-size: 13px;" + ) # --------------------------------------------------------------- busy UI def set_busy(self, busy: bool, message: str = "") -> None: - self.download_btn.setEnabled(not busy) - self.run_btn.setEnabled(not busy) - self.delete_btn.setEnabled(not busy) + self._busy = busy if busy and message: self.status_label.setText(message) self.status_label.setStyleSheet("color: #d0d060; font-size: 11px;") diff --git a/jlauncher/ui/main_window.py b/jlauncher/ui/main_window.py index 8b647ae..c1497c4 100644 --- a/jlauncher/ui/main_window.py +++ b/jlauncher/ui/main_window.py @@ -53,8 +53,7 @@ class MainWindow(QMainWindow): 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) + row.activated.connect(self._on_activate) row.delete_requested.connect(self._on_delete) self.rows[game.id] = row list_layout.addWidget(row) @@ -151,6 +150,14 @@ class MainWindow(QMainWindow): # --------------------------------------------------------------- accions + def _on_activate(self, game: Game) -> None: + """Clic sobre la fila: descarrega/actualitza si cal, si no juga.""" + row = self.rows[game.id] + if row.primary_action_is_download(): + self._on_download(game) + else: + self._on_run(game) + def _on_download(self, game: Game) -> None: row = self.rows[game.id] row.set_busy(True, "Descarregant…")