diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index ec9a45bc..37a703ca 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -27,8 +27,7 @@ from rare.shared import ( ImageManagerSingleton, ) from rare.shared.image_manager import ImageSize -from rare.shared.workers.verify import VerifyWorker -from rare.shared.workers.move import MoveWorker +from rare.shared.workers import VerifyWorker, MoveWorker from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.utils.misc import get_size from rare.utils.steam_grades import SteamWorker @@ -267,8 +266,8 @@ class GameInfo(QWidget): ) copy_worker.signals.progress.connect(self.__on_move_progress) - copy_worker.signals.finished.connect(self.set_new_game) - copy_worker.signals.no_space_left.connect(self.warn_no_space_left) + copy_worker.signals.result.connect(self.set_new_game) + copy_worker.signals.error.connect(self.warn_no_space_left) QThreadPool.globalInstance().start(copy_worker) def move_helper_clean_up(self): @@ -300,6 +299,10 @@ class GameInfo(QWidget): def show_menu_after_browse(self): self.ui.move_button.showMenu() + @pyqtSlot() + def __update_ui(self): + pass + @pyqtSlot(str) @pyqtSlot(RareGame) def update_game(self, rgame: Union[RareGame, str]): @@ -307,24 +310,39 @@ class GameInfo(QWidget): rgame = self.rcore.get_game(rgame) if self.rgame is not None: - if (worker := self.rgame.__worker) is not None: + if (worker := self.rgame.worker) is not None: if isinstance(worker, VerifyWorker): try: worker.signals.progress.disconnect(self.__on_verify_progress) except TypeError as e: - logger.warning(f"{self.rgame.app_title} verify worker: {e}") + logger.warning(f"{self.rgame.app_name} verify worker: {e}") + if isinstance(worker, MoveWorker): + try: + worker.signals.progress.disconnect(self.__on_move_progress) + except TypeError as e: + logger.warning(f"{self.rgame.app_name} move worker: {e}") + self.rgame.signals.widget.update.disconnect(self.__update_ui) self.rgame.signals.game.installed.disconnect(self.update_game) self.rgame.signals.game.uninstalled.disconnect(self.update_game) + self.rgame = rgame + + self.rgame.signals.widget.update.disconnect(self.__update_ui) self.rgame.signals.game.installed.connect(self.update_game) self.rgame.signals.game.uninstalled.connect(self.update_game) - if (worker := self.rgame.__worker) is not None: + if (worker := self.rgame.worker) is not None: if isinstance(worker, VerifyWorker): self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page) self.ui.verify_progress.setValue(self.rgame.progress) worker.signals.progress.connect(self.__on_verify_progress) - else: - self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page) + else: + self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page) + if isinstance(worker, MoveWorker): + self.ui.move_stack.setCurrentWidget(self.ui.move_progress_page) + self.ui.move_progress.setValue(self.rgame.progress) + worker.signals.progress.connect(self.__on_move_progress) + else: + self.ui.move_stack.setCurrentWidget(self.ui.move_button_page) self.title.setTitle(self.rgame.app_title) self.image.setPixmap(rgame.pixmap) @@ -383,7 +401,7 @@ class GameInfo(QWidget): self.ui.move_stack.setCurrentWidget(self.ui.move_button_page) # If a game is verifying or moving, disable both verify and moving buttons. - if rgame.__worker is not None: + if rgame.worker is not None: self.ui.verify_button.setEnabled(False) self.ui.move_button.setEnabled(False) if self.is_moving: diff --git a/rare/models/game.py b/rare/models/game.py index a2bd8fea..5def46a5 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -14,8 +14,6 @@ from rare.lgndr.core import LegendaryCore from rare.models.install import InstallOptionsModel, UninstallOptionsModel from rare.shared.game_process import GameProcess from rare.shared.image_manager import ImageManager -from rare.shared.workers.move import MoveWorker -from rare.shared.workers.verify import VerifyWorker from rare.utils.misc import get_rare_executable from rare.utils.paths import data_dir @@ -118,10 +116,11 @@ class RareGame(QObject): if self.has_update: logger.info(f"Update available for game: {self.app_name} ({self.app_title})") - self.progress: int = 0 self.__worker: Optional[QRunnable] = None - self.__state = RareGame.State.IDLE + self.progress: int = 0 + self.signals.progress.start.connect(lambda: self.__on_progress_update(0)) + self.signals.progress.update.connect(self.__on_progress_update) self.game_process = GameProcess(self.game) self.game_process.launched.connect(self.__game_launched) @@ -130,18 +129,15 @@ class RareGame(QObject): self.game_process.connect_to_server(on_startup=True) # self.grant_date(True) + def __on_progress_update(self, progress: int): + self.progress = progress + @property def worker(self) -> Optional[QRunnable]: return self.__worker @worker.setter def worker(self, worker: Optional[QRunnable]): - if worker is None: - self.state = RareGame.State.IDLE - if isinstance(worker, VerifyWorker): - self.state = RareGame.State.VERIFYING - if isinstance(worker, MoveWorker): - self.state = RareGame.State.MOVING self.__worker = worker @property @@ -497,21 +493,13 @@ class RareGame(QObject): def refresh_pixmap(self): self.image_manager.download_image(self.game, self.set_pixmap, 0, True) - def start_progress(self): - self.signals.progress.start.emit() - - def update_progress(self, progress: int): - self.progress = progress - self.signals.progress.update.emit(progress) - - def finish_progress(self, fail: bool, miss: int, app: str): - self.set_installed(True) - self.signals.progress.finish.emit(fail) - - def install(self): + def install(self) -> bool: + if not self.is_idle: + return False self.signals.game.install.emit( InstallOptionsModel(app_name=self.app_name) ) + return True def repair(self, repair_and_update): self.signals.game.install.emit( @@ -520,10 +508,13 @@ class RareGame(QObject): ) ) - def uninstall(self): + def uninstall(self) -> bool: + if not self.is_idle: + return False self.signals.game.uninstall.emit( UninstallOptionsModel(app_name=self.app_name) ) + return True def launch( self, @@ -532,9 +523,9 @@ class RareGame(QObject): wine_bin: Optional[str] = None, wine_pfx: Optional[str] = None, ask_sync_saves: bool = False, - ): + ) -> bool: if not self.can_launch: - return + return False cmd_line = get_rare_executable() executable, args = cmd_line[0], cmd_line[1:] @@ -554,6 +545,7 @@ class RareGame(QObject): QProcess.startDetached(executable, args) logger.info(f"Start new Process: ({executable} {' '.join(args)})") self.game_process.connect_to_server(on_startup=False) + return True class RareEosOverlay(QObject): diff --git a/rare/models/signals.py b/rare/models/signals.py index c3906c33..8377e2d3 100644 --- a/rare/models/signals.py +++ b/rare/models/signals.py @@ -19,6 +19,8 @@ class GlobalSignals: overlay_installed = pyqtSignal() # none update_tray = pyqtSignal() + # none + update_statusbar = pyqtSignal() class GameSignals(QObject): # model diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index da99bead..1a512d56 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -3,9 +3,9 @@ import os from argparse import Namespace from itertools import chain from logging import getLogger -from typing import Optional, Dict, Iterator, Callable +from typing import Optional, Dict, Iterator, Callable, List -from PyQt5.QtCore import QObject +from PyQt5.QtCore import QObject, QThreadPool from legendary.lfs.eos import EOSOverlayApp from legendary.models.game import Game, SaveGameFile @@ -13,6 +13,7 @@ from rare.lgndr.core import LegendaryCore from rare.models.apiresults import ApiResults from rare.models.game import RareGame, RareEosOverlay from rare.models.signals import GlobalSignals +from .workers import QueueWorker, VerifyWorker, MoveWorker from .image_manager import ImageManager logger = getLogger("RareCore") @@ -36,12 +37,30 @@ class RareCore(QObject): self.core(init=True) self.image_manager(init=True) + self.queue_workers: List[QueueWorker] = [] + self.queue_threadpool = QThreadPool() + self.queue_threadpool.setMaxThreadCount(2) + self.__games: Dict[str, RareGame] = {} self.__eos_overlay_rgame = RareEosOverlay(self.__core, self.__image_manager, EOSOverlayApp) RareCore._instance = self + def enqueue_worker(self, rgame: RareGame, worker: QueueWorker): + if isinstance(worker, VerifyWorker): + rgame.state = RareGame.State.VERIFYING + if isinstance(worker, MoveWorker): + rgame.state = RareGame.State.MOVING + rgame.worker = worker + worker.feedback.started.connect(self.__signals.application.update_statubar) + worker.feedback.finished.connect(lambda: self.queue_workers.remove(worker)) + self.queue_workers.append(worker) + self.queue_threadpool.start(worker, 0) + + def queue_info(self) -> List: + return [w.worker_info() for w in self.queue_workers] + @staticmethod def instance() -> 'RareCore': if RareCore._instance is None: diff --git a/rare/shared/workers/__init__.py b/rare/shared/workers/__init__.py index e69de29b..82df4c0a 100644 --- a/rare/shared/workers/__init__.py +++ b/rare/shared/workers/__init__.py @@ -0,0 +1,5 @@ +from .install_info import InstallInfoWorker +from .move import MoveWorker +from .uninstall import UninstallWorker +from .verify import VerifyWorker +from .worker import Worker, QueueWorker diff --git a/rare/shared/workers/move.py b/rare/shared/workers/move.py index 5027ae5c..d9896dc6 100644 --- a/rare/shared/workers/move.py +++ b/rare/shared/workers/move.py @@ -2,20 +2,20 @@ import os import shutil from pathlib import Path -from PyQt5.QtCore import pyqtSignal, QRunnable, QObject +from PyQt5.QtCore import pyqtSignal, QObject from legendary.lfs.utils import validate_files from legendary.models.game import VerifyResult, InstalledGame from rare.lgndr.core import LegendaryCore -from .worker import Worker +from .worker import QueueWorker # noinspection PyUnresolvedReferences -class MoveWorker(Worker): +class MoveWorker(QueueWorker): class Signals(QObject): progress = pyqtSignal(int) - finished = pyqtSignal(str) - no_space_left = pyqtSignal() + result = pyqtSignal(str) + error = pyqtSignal() def __init__( self, @@ -37,6 +37,9 @@ class MoveWorker(Worker): self.file_list = None self.total: int = 0 + def worker_info(self): + return None + def run_real(self): root_directory = Path(self.install_path) self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file()) diff --git a/rare/shared/workers/verify.py b/rare/shared/workers/verify.py index 638e32bc..db4f3f17 100644 --- a/rare/shared/workers/verify.py +++ b/rare/shared/workers/verify.py @@ -10,12 +10,12 @@ from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.arguments import LgndrVerifyGameArgs from rare.lgndr.glue.monkeys import LgndrIndirectStatus from rare.models.game import RareGame -from .worker import Worker +from .worker import QueueWorker logger = getLogger("VerifyWorker") -class VerifyWorker(Worker): +class VerifyWorker(QueueWorker): class Signals(QObject): progress = pyqtSignal(RareGame, int, int, float, float) result = pyqtSignal(RareGame, bool, int, int) @@ -35,6 +35,9 @@ class VerifyWorker(Worker): self.rgame.signals.progress.update.emit(num * 100 // total) self.signals.progress.emit(self.rgame, num, total, percentage, speed) + def worker_info(self): + return None + def run_real(self): self.rgame.signals.progress.start.emit() cli = LegendaryCLI(self.core) diff --git a/rare/shared/workers/worker.py b/rare/shared/workers/worker.py index f72b50da..fdb1d535 100644 --- a/rare/shared/workers/worker.py +++ b/rare/shared/workers/worker.py @@ -1,11 +1,21 @@ import sys from abc import abstractmethod +from enum import IntEnum from typing import Optional -from PyQt5.QtCore import QRunnable, QObject, pyqtSlot +from PyQt5.QtCore import QRunnable, QObject, pyqtSlot, pyqtSignal class Worker(QRunnable): + """ + Base QRunnable class. + + This class provides a base for QRunnables with signals that are automatically deleted. + + To use this class you have to assign the signals object of your concrete implementation + to the `Worker.signals` attribute and implement `Worker.run_real()` + """ + def __init__(self): sys.excepthook = sys.__excepthook__ super(Worker, self).__init__() @@ -30,3 +40,41 @@ class Worker(QRunnable): def run(self): self.run_real() self.signals.deleteLater() + + +class QueueWorker(Worker): + """ + Base queueable worker class + + This class is a specialization of the `Worker` class. It provides feedback signals to know + if a worker has started or finished. + + To use this class you have to assign the signals object of your concrete implementation + to the `QueueWorker.signals` attribute, implement `QueueWorker.run_real()` and `QueueWorker.worker_info()` + """ + + class State(IntEnum): + UNDEFINED = 0 + QUEUED = 1 + ACTIVE = 2 + + class Signals(QObject): + started = pyqtSignal() + finished = pyqtSignal() + + def __init__(self): + super(QueueWorker, self).__init__() + self.feedback = QueueWorker.Signals() + self.state = QueueWorker.State.QUEUED + + @pyqtSlot() + def run(self): + self.state = QueueWorker.State.ACTIVE + self.feedback.started.emit() + super(QueueWorker, self).run() + self.feedback.finished.emit() + self.feedback.deleteLater() + + @abstractmethod + def worker_info(self): + pass \ No newline at end of file