La fila és el botó: clic descarrega/actualitza o juga, amb text d'acció

This commit is contained in:
2026-05-29 22:03:41 +02:00
parent 694d67f11e
commit 0334e79480
2 changed files with 74 additions and 60 deletions
+65 -58
View File
@@ -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 __future__ import annotations
from pathlib import Path 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.QtGui import QPalette, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFrame, QFrame,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QPushButton,
QStyle,
QVBoxLayout, QVBoxLayout,
) )
@@ -23,38 +29,43 @@ ICON_SIZE = 64
class GameRow(QFrame): 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 activated = Signal(object) # Game — clic sobre la fila (acció principal)
run_requested = Signal(object) # Game delete_requested = Signal(object) # Game — esborrar la descàrrega local
delete_requested = Signal(object) # Game
def __init__(self, game: Game, root: Path, parent=None) -> None: def __init__(self, game: Game, root: Path, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.game = game self.game = game
self.root = root self.root = root
self._update_available = False self._update_available = False
self._busy = False
self.setObjectName("gameRow") self.setObjectName("gameRow")
self.setFrameShape(QFrame.StyledPanel) 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 = QHBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8) layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(12) layout.setSpacing(12)
# --- Icono --- # --- Icona del joc ---
self.icon_label = QLabel() self.icon_label = QLabel()
self.icon_label.setFixedSize(ICON_SIZE, ICON_SIZE) self.icon_label.setFixedSize(ICON_SIZE, ICON_SIZE)
self.icon_label.setAlignment(Qt.AlignCenter) self.icon_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.icon_label) layout.addWidget(self.icon_label)
# --- Texto (nombre + descripción + estado) --- # --- Text (nom + descripció + estat) ---
text_box = QVBoxLayout() text_box = QVBoxLayout()
text_box.setSpacing(2) text_box.setSpacing(2)
self.name_label = QLabel(game.name) self.name_label = QLabel(game.name)
self.name_label.setStyleSheet("font-weight: bold; font-size: 14px;") self.name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
self.desc_label = QLabel("") self.desc_label = QLabel("")
self.desc_label.setWordWrap(True) 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.desc_label.setForegroundRole(QPalette.PlaceholderText)
self.status_label = QLabel("") self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;") self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;")
@@ -64,47 +75,51 @@ class GameRow(QFrame):
text_box.addStretch(1) text_box.addStretch(1)
layout.addLayout(text_box, stretch=1) layout.addLayout(text_box, stretch=1)
# --- Botons (icones compactes amb tooltip; visibilitat segons l'estat) --- # --- Indicador d'acció (passiu, text): què farà el clic ---
style = self.style() self.action_label = QLabel()
self._icon_download = style.standardIcon(QStyle.SP_ArrowDown) self.action_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self._icon_update = style.standardIcon(QStyle.SP_BrowserReload) self.action_label.setMinimumWidth(96)
layout.addWidget(self.action_label)
self.download_btn = QPushButton() # Els fills no intercepten el ratolí: tot clic arriba a la fila.
self.run_btn = QPushButton() for child in (
self.run_btn.setIcon(style.standardIcon(QStyle.SP_MediaPlay)) self.icon_label,
self.run_btn.setToolTip("Juga") self.name_label,
self.delete_btn = QPushButton() self.desc_label,
self.delete_btn.setIcon(style.standardIcon(QStyle.SP_TrashIcon)) self.status_label,
self.delete_btn.setToolTip("Esborra") self.action_label,
for btn in (self.download_btn, self.run_btn, self.delete_btn): ):
btn.setFixedSize(36, 30) child.setAttribute(Qt.WA_TransparentForMouseEvents)
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)
self.refresh() 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: def is_installed(self) -> bool:
return (repo_dir(self.root, self.game.id) / ".git").exists() 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: 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._update_available = available
self.refresh() self.refresh()
def refresh(self) -> None: 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) meta = load_meta(self.root, self.game.id)
self._set_icon() self._set_icon()
self.desc_label.setText(meta.description or "(sense descripció encara)") self.desc_label.setText(meta.description or "(sense descripció encara)")
@@ -128,40 +143,32 @@ class GameRow(QFrame):
) )
def _set_status(self, meta: GameMeta) -> None: def _set_status(self, meta: GameMeta) -> None:
installed = self.is_installed() if not 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:
self.status_label.setText("No descarregat") self.status_label.setText("No descarregat")
self.status_label.setStyleSheet("color: #cc8855; font-size: 11px;") self.status_label.setStyleSheet("color: #cc8855; font-size: 11px;")
self._set_action("Descarrega", "#4a90d9")
elif self._update_available: elif self._update_available:
self.status_label.setText("⬆ Actualització disponible") self.status_label.setText("⬆ Actualització disponible")
self.status_label.setStyleSheet( self.status_label.setStyleSheet(
"color: #e0a030; font-weight: bold; font-size: 11px;" "color: #e0a030; font-weight: bold; font-size: 11px;"
) )
self._set_action("Actualitza", "#e0a030")
else: else:
ver = f" {meta.version}" if meta.version else "" ver = f" {meta.version}" if meta.version else ""
self.status_label.setText(f"Descarregat{ver}") self.status_label.setText(f"Descarregat{ver}")
self.status_label.setStyleSheet("color: #6fae6f; font-size: 11px;") 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 # --------------------------------------------------------------- busy UI
def set_busy(self, busy: bool, message: str = "") -> None: def set_busy(self, busy: bool, message: str = "") -> None:
self.download_btn.setEnabled(not busy) self._busy = busy
self.run_btn.setEnabled(not busy)
self.delete_btn.setEnabled(not busy)
if busy and message: if busy and message:
self.status_label.setText(message) self.status_label.setText(message)
self.status_label.setStyleSheet("color: #d0d060; font-size: 11px;") self.status_label.setStyleSheet("color: #d0d060; font-size: 11px;")
+9 -2
View File
@@ -53,8 +53,7 @@ class MainWindow(QMainWindow):
list_layout.setSpacing(6) list_layout.setSpacing(6)
for game in config.games: for game in config.games:
row = GameRow(game, root) row = GameRow(game, root)
row.download_requested.connect(self._on_download) row.activated.connect(self._on_activate)
row.run_requested.connect(self._on_run)
row.delete_requested.connect(self._on_delete) row.delete_requested.connect(self._on_delete)
self.rows[game.id] = row self.rows[game.id] = row
list_layout.addWidget(row) list_layout.addWidget(row)
@@ -151,6 +150,14 @@ class MainWindow(QMainWindow):
# --------------------------------------------------------------- accions # --------------------------------------------------------------- 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: def _on_download(self, game: Game) -> None:
row = self.rows[game.id] row = self.rows[game.id]
row.set_busy(True, "Descarregant…") row.set_busy(True, "Descarregant…")