Consola amb 3 estats (mostra/auto-amaga/amaga), animada i més alta
- Submenú Opcions > Consola: Mostra / Auto-amaga / Amaga, persistit a settings.json (console_mode). Es reemplaça el QSplitter per un panell col·lapsable amb alçada animada (QPropertyAnimation, easing InOutCubic) i més alçada (220px). - Mode auto: la consola es desplega amb activitat (worker o nova línia de log) i es replega sola tras un marge sense activitat. - Pills robustos al canvi de tema: color de text concret des de la paleta en comptes de palette(...) (que Qt cacheja), i pills_box sense fons propi perquè no pinti cap banda darrere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+122
-11
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QThreadPool, Qt, QTimer
|
||||
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, QThreadPool, Qt, QTimer
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -15,7 +15,6 @@ from PySide6.QtWidgets import (
|
||||
QPlainTextEdit,
|
||||
QProgressBar,
|
||||
QScrollArea,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@@ -30,6 +29,14 @@ from .game_row import GameRow
|
||||
APP_NAME = "Jail Launcher"
|
||||
WINDOW_TITLE = f"© 2026 {APP_NAME} — JailDesigner"
|
||||
|
||||
CONSOLE_HEIGHT = 220 # alçada de la consola desplegada (px)
|
||||
CONSOLE_ANIM_MS = 220 # durada de l'animació de desplegar/replegar
|
||||
CONSOLE_IDLE_MS = 1800 # marge sense activitat abans de replegar en mode auto
|
||||
|
||||
CONSOLE_SHOW = "show"
|
||||
CONSOLE_AUTO = "auto"
|
||||
CONSOLE_HIDE = "hide"
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, config: Config, root: Path, parent=None) -> None:
|
||||
@@ -57,7 +64,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._build_menu()
|
||||
|
||||
splitter = QSplitter(Qt.Vertical)
|
||||
central = QWidget()
|
||||
root_layout = QVBoxLayout(central)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.setSpacing(0)
|
||||
|
||||
# --- Lista de juegos con scroll ---
|
||||
list_container = QWidget()
|
||||
@@ -75,18 +85,18 @@ class MainWindow(QMainWindow):
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(list_container)
|
||||
splitter.addWidget(scroll)
|
||||
root_layout.addWidget(scroll, stretch=1)
|
||||
|
||||
# --- Panel de log ---
|
||||
# --- Panel de log (consola colapsable amb alçada animada) ---
|
||||
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.log_view.setMinimumHeight(0)
|
||||
root_layout.addWidget(self.log_view)
|
||||
|
||||
self.setCentralWidget(splitter)
|
||||
self.setCentralWidget(central)
|
||||
self._setup_console()
|
||||
|
||||
# Barra de progrés (cantonada de la status bar) per a la comprovació d'updates;
|
||||
# amagada fins que arrenca una comprovació.
|
||||
@@ -118,6 +128,64 @@ class MainWindow(QMainWindow):
|
||||
stall_time=s.git_stall_time,
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------- consola
|
||||
|
||||
def _setup_console(self) -> None:
|
||||
"""Prepara la animació d'alçada i l'estat inicial segons console_mode."""
|
||||
self._console_open = False
|
||||
self._console_anim = QPropertyAnimation(self.log_view, b"maximumHeight", self)
|
||||
self._console_anim.setDuration(CONSOLE_ANIM_MS)
|
||||
self._console_anim.setEasingCurve(QEasingCurve.InOutCubic)
|
||||
self._console_anim.finished.connect(self._on_console_anim_done)
|
||||
|
||||
# Timer que replega la consola en mode auto després d'un marge sense activitat.
|
||||
self._console_idle_timer = QTimer(self)
|
||||
self._console_idle_timer.setSingleShot(True)
|
||||
self._console_idle_timer.setInterval(CONSOLE_IDLE_MS)
|
||||
self._console_idle_timer.timeout.connect(self._on_console_idle)
|
||||
|
||||
# Estat inicial sense animació: oberta només en mode "show".
|
||||
self.log_view.setMaximumHeight(0)
|
||||
self.log_view.hide()
|
||||
if self.settings.console_mode == CONSOLE_SHOW:
|
||||
self._set_console_open(True, animated=False)
|
||||
|
||||
def _set_console_open(self, open_: bool, animated: bool = True) -> None:
|
||||
if open_ == self._console_open:
|
||||
return
|
||||
self._console_open = open_
|
||||
target = CONSOLE_HEIGHT if open_ else 0
|
||||
if open_:
|
||||
self.log_view.show() # visible abans d'animar l'obertura
|
||||
self._console_anim.stop()
|
||||
if not animated:
|
||||
self.log_view.setMaximumHeight(target)
|
||||
if not open_:
|
||||
self.log_view.hide()
|
||||
return
|
||||
self._console_anim.setStartValue(self.log_view.maximumHeight())
|
||||
self._console_anim.setEndValue(target)
|
||||
self._console_anim.start()
|
||||
|
||||
def _on_console_anim_done(self) -> None:
|
||||
if not self._console_open:
|
||||
self.log_view.hide() # replegada del tot: treure-la del layout
|
||||
|
||||
def _on_console_idle(self) -> None:
|
||||
if self.settings.console_mode == CONSOLE_AUTO and not self._workers:
|
||||
self._set_console_open(False)
|
||||
|
||||
def _console_activity_started(self) -> None:
|
||||
"""Hi ha activitat (worker o log): en mode auto, desplega i atura el timer."""
|
||||
if self.settings.console_mode == CONSOLE_AUTO:
|
||||
self._console_idle_timer.stop()
|
||||
self._set_console_open(True)
|
||||
|
||||
def _console_activity_maybe_ended(self) -> None:
|
||||
"""Si no queden workers actius, en mode auto arrenca el compte enrere per replegar."""
|
||||
if self.settings.console_mode == CONSOLE_AUTO and not self._workers:
|
||||
self._console_idle_timer.start()
|
||||
|
||||
# --------------------------------------------------------------- menú
|
||||
|
||||
def _build_menu(self) -> None:
|
||||
@@ -141,6 +209,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
menu.addSeparator()
|
||||
self._build_theme_menu(menu)
|
||||
self._build_console_menu(menu)
|
||||
|
||||
menu.addSeparator()
|
||||
self.action_delete = QAction("Esborra un joc", self, checkable=True)
|
||||
@@ -169,6 +238,36 @@ class MainWindow(QMainWindow):
|
||||
group.addAction(action)
|
||||
submenu.addAction(action)
|
||||
|
||||
def _build_console_menu(self, parent_menu) -> None:
|
||||
"""Submenú Consola amb tres estats exclusius: Mostra / Auto-amaga / Amaga."""
|
||||
submenu = parent_menu.addMenu("Consola")
|
||||
group = QActionGroup(self)
|
||||
group.setExclusive(True)
|
||||
options = [
|
||||
("Mostra", CONSOLE_SHOW),
|
||||
("Auto-amaga", CONSOLE_AUTO),
|
||||
("Amaga", CONSOLE_HIDE),
|
||||
]
|
||||
for label, mode in options:
|
||||
action = QAction(label, self, checkable=True)
|
||||
action.setChecked(self.settings.console_mode == mode)
|
||||
action.triggered.connect(
|
||||
lambda _checked, m=mode: self._on_console_mode_selected(m)
|
||||
)
|
||||
group.addAction(action)
|
||||
submenu.addAction(action)
|
||||
|
||||
def _on_console_mode_selected(self, mode: str) -> None:
|
||||
self.settings.console_mode = mode
|
||||
save_settings(self.settings)
|
||||
self._console_idle_timer.stop()
|
||||
if mode == CONSOLE_SHOW:
|
||||
self._set_console_open(True)
|
||||
elif mode == CONSOLE_HIDE:
|
||||
self._set_console_open(False)
|
||||
else: # auto: oberta si hi ha activitat, si no replegada
|
||||
self._set_console_open(bool(self._workers))
|
||||
|
||||
def _on_theme_selected(self, mode: str) -> None:
|
||||
self.settings.theme = mode
|
||||
save_settings(self.settings)
|
||||
@@ -256,14 +355,26 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _log(self, text: str) -> None:
|
||||
self.log_view.appendPlainText(text)
|
||||
# En mode auto, qualsevol línia desplega la consola; si no hi ha cap worker
|
||||
# actiu (p.ex. un missatge solt), arrenca el compte enrere per replegar-la.
|
||||
if self.settings.console_mode == CONSOLE_AUTO:
|
||||
self._console_activity_started()
|
||||
if not self._workers:
|
||||
self._console_idle_timer.start()
|
||||
|
||||
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))
|
||||
self._console_activity_started()
|
||||
|
||||
def _done(*_):
|
||||
self._workers.discard(worker)
|
||||
self._console_activity_maybe_ended()
|
||||
|
||||
worker.signals.finished.connect(_done)
|
||||
worker.signals.error.connect(_done)
|
||||
|
||||
# --------------------------------------------------------------- accions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user