From 76cd33054a7069bd88c9e506ec3a771b5e549847 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 2 Jul 2022 11:48:33 +0300 Subject: [PATCH 1/2] EGLSyncListGroup: Move message box outside of thread to prevent "parent in other thread" crash --- .../tabs/games/import_sync/egl_sync_group.py | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/rare/components/tabs/games/import_sync/egl_sync_group.py b/rare/components/tabs/games/import_sync/egl_sync_group.py index eb096f5d..56df606c 100644 --- a/rare/components/tabs/games/import_sync/egl_sync_group.py +++ b/rare/components/tabs/games/import_sync/egl_sync_group.py @@ -1,9 +1,9 @@ import os import platform from logging import getLogger -from typing import Tuple, Iterable +from typing import Tuple, Iterable, List -from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot +from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton @@ -200,12 +200,16 @@ class EGLSyncListItem(QListWidgetItem): class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup): + action_errors = pyqtSignal(list) + def __init__(self, export: bool, parent=None): super(EGLSyncListGroup, self).__init__(parent=parent) self.setupUi(self) self.core = LegendaryCoreSingleton() self.signals = GlobalSignalsSingleton() self.list.setProperty("noBorder", 1) + # TODO: Convert the CSS and the code to adhere to NoFrame + # self.list.setFrameShape(self.list.NoFrame) self.export = export @@ -232,6 +236,8 @@ class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup): self.action_button.clicked.connect(self.action) + self.action_errors.connect(self.show_errors) + def has_selected(self): for item in self.items: if item.is_checked(): @@ -258,8 +264,8 @@ class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup): self.buttons_widget.setVisible(enabled and bool(self.list.count())) def action(self): - imported = list() - errors = list() + imported: List = [] + errors: List = [] for item in self.items: if item.is_checked(): if e := item.action(): @@ -271,13 +277,17 @@ class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup): self.signals.update_gamelist.emit(imported) self.populate(True) if errors: - QMessageBox.warning( - self.parent(), - self.tr("The following errors occurred while {}.").format( - self.tr("exporting") if self.export else self.tr("importing") - ), - "\n".join(errors), - ) + self.action_errors.emit(errors) + + @pyqtSlot(list) + def show_errors(self, errors: List): + QMessageBox.warning( + self.parent(), + self.tr("The following errors occurred while {}.").format( + self.tr("exporting") if self.export else self.tr("importing") + ), + "\n".join(errors), + ) @property def items(self) -> Iterable[EGLSyncListItem]: From 0d6c9a850569ca39d2c0e1987961faa9c1fa4b5c Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 2 Jul 2022 19:22:12 +0300 Subject: [PATCH 2/2] ImportGroup: Show messagebox when importing multiple games (scanning install folder) I moved around a few things because the separate lists felt like they would make the handling a bit complicated. --- .../tabs/games/import_sync/import_group.py | 147 +++++++++++------- .../tabs/games/import_sync/import_group.py | 46 +++--- .../tabs/games/import_sync/import_group.ui | 51 +++--- 3 files changed, 138 insertions(+), 106 deletions(-) diff --git a/rare/components/tabs/games/import_sync/import_group.py b/rare/components/tabs/games/import_sync/import_group.py index f606094a..18ca268a 100644 --- a/rare/components/tabs/games/import_sync/import_group.py +++ b/rare/components/tabs/games/import_sync/import_group.py @@ -1,13 +1,14 @@ import json import os from dataclasses import dataclass +from enum import IntEnum from logging import getLogger from pathlib import Path from typing import List, Tuple, Optional -from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThreadPool +from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThreadPool, pyqtSlot from PyQt5.QtGui import QStandardItemModel -from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, QApplication, QMessageBox +from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup @@ -34,22 +35,22 @@ def find_app_name(path: str, core) -> Optional[str]: return None -@dataclass -class ResultGame: - app_name: str - error: Optional[str] = None +class ImportResult(IntEnum): + ERROR = 0 + FAILED = 1 + SUCCESS = 2 @dataclass -class Result: - successful_games: List[ResultGame] = None - failed_games: List[ResultGame] = None - error_messages: List[str] = None +class ImportedGame: + result: ImportResult + app_name: Optional[str] = None + message: Optional[str] = None class ImportWorker(QRunnable): class Signals(QObject): - finished = pyqtSignal(Result) + finished = pyqtSignal(list) progress = pyqtSignal(int) def __init__(self, path: str, import_folder: bool = False, app_name: str = None): @@ -59,39 +60,37 @@ class ImportWorker(QRunnable): self.path = Path(path) self.import_folder = import_folder self.app_name = app_name - self.tr = lambda message: QApplication.translate("ImportThread", message) + self.tr = lambda message: qApp.translate("ImportThread", message) def run(self) -> None: - result = Result([], [], []) + result_list: List = [] if self.import_folder: - number_of_folders = len(list(self.path.iterdir())) - for i, child in enumerate(self.path.iterdir()): + folders = [i for i in self.path.iterdir() if i.is_dir()] + number_of_folders = len(folders) + for i, child in enumerate(folders): if not child.is_dir(): continue - if (app_name := find_app_name(str(child), self.core)) is not None: - logger.debug(f"Found app_name {app_name} for {child}") - err = self.__import_game(app_name, child) - if err: - result.failed_games.append(ResultGame(app_name, err)) - else: - result.successful_games.append(ResultGame(app_name)) - else: - result.error_messages.append(self.tr("Could not find AppName for {}").format(child)) + result = self.__try_import(child, None) + result_list.append(result) self.signals.progress.emit(int(100 * i // number_of_folders)) else: - if not self.app_name: - # try to find app name - if a_n := find_app_name(str(self.path), self.core): - self.app_name = a_n - else: - result.error_messages.append(self.tr("Could not find AppName for {}").format(str(self.path))) - return - err = self.__import_game(self.app_name, self.path) + result = self.__try_import(self.path, self.app_name) + result_list.append(result) + self.signals.finished.emit(result_list) + + def __try_import(self, path: Path, app_name: str = None) -> ImportedGame: + result = ImportedGame(ImportResult.ERROR, None, None) + if app_name or (app_name := find_app_name(str(path), self.core)): + result.app_name = app_name + err = self.__import_game(app_name, path) if err: - result.failed_games.append(ResultGame(self.app_name, err)) + result.result = ImportResult.FAILED + result.message = err else: - result.successful_games.append(ResultGame(self.app_name)) - self.signals.finished.emit(result) + result.result = ImportResult.SUCCESS + else: + result.message = self.tr("Could not find AppName for {}").format(str(path)) + return result def __import_game(self, app_name: str, path: Path) -> str: if not (err := legendary_utils.import_game(self.core, app_name=app_name, path=str(path))): @@ -205,12 +204,12 @@ class ImportGroup(QGroupBox): return False, path, "" def path_changed(self, path): - self.ui.info_label.setText(str()) + self.ui.info_label.setText("") self.ui.import_folder_check.setChecked(False) if self.path_edit.is_valid: self.app_name_edit.setText(find_app_name(path, self.core)) else: - self.app_name_edit.setText(str()) + self.app_name_edit.setText("") def app_name_edit_cb(self, text) -> Tuple[bool, str, str]: if not text: @@ -221,7 +220,7 @@ class ImportGroup(QGroupBox): return False, text, IndicatorLineEdit.reasons.game_not_installed def app_name_changed(self, text): - self.ui.info_label.setText(str()) + self.ui.info_label.setText("") if self.app_name_edit.is_valid: self.ui.import_button.setEnabled(True) else: @@ -235,33 +234,63 @@ class ImportGroup(QGroupBox): worker.signals.progress.connect(self.import_progress) self.threadpool.start(worker) - def import_finished(self, result: Result): - logger.info(f"Import finished: {result.__dict__}") - if result.successful_games: - self.signals.update_gamelist.emit([i.app_name for i in result.successful_games]) - if len(result.successful_games) == 1: + @pyqtSlot(list) + def import_finished(self, result: List): + logger.info(f"Import finished: {result}") + + self.signals.update_gamelist.emit([r.app_name for r in result if r.result == ImportResult.SUCCESS]) + + for failed in (f for f in result if f.result == ImportResult.FAILED): + igame = self.core.get_installed_game(failed.app_name) + if igame and igame.version != self.core.get_asset(igame.app_name, igame.platform, False).build_version: + # update available + self.signals.add_download.emit(igame.app_name) + self.signals.update_download_tab_text.emit() + + if len(result) == 1: + res = result[0] + if res.result == ImportResult.SUCCESS: self.ui.info_label.setText( - self.tr(f"{self.core.get_game(result.successful_games[0].app_name).app_title} imported successfully: ") + self.tr("{} was imported successfully").format(self.core.get_game(res.app_name).app_title) + ) + elif res.result == ImportResult.FAILED: + self.ui.info_label.setText( + self.tr("Failed: {}").format(res.message) ) else: self.ui.info_label.setText( - self.tr("Imported {} games successfully".format(len(result.successful_games))) + self.tr("Error: {}").format(res.message) ) - for res_game in result.failed_games: - igame = self.core.get_installed_game(res_game.app_name) - if igame.version != self.core.get_asset(igame.app_name, igame.platform, False).build_version: - # update available - self.signals.add_download.emit(igame.app_name) - self.signals.update_download_tab_text.emit() - - if result.failed_games: - self.ui.info_label.setText( - self.tr(f"Failed to import: " - f"{', '.join([self.core.get_game(i.app_name).app_title for i in result.failed_games])}") + else: + success = [r for r in result if r.result == ImportResult.SUCCESS] + failure = [r for r in result if r.result == ImportResult.FAILED] + errored = [r for r in result if r.result == ImportResult.ERROR] + messagebox = QMessageBox( + QMessageBox.Information, + self.tr("Import summary"), + self.tr( + "Tried to import {} folders.\n\n" + "Successfully imported {} games, failed to import {} games and {} errors occurred" + ).format(len(success) + len(failure) + len(errored), len(success), len(failure), len(errored)), + buttons=QMessageBox.StandardButton.Close, + parent=self, ) - if result.error_messages: - QMessageBox.warning(self, self.tr("Error"), self.tr("\n".join(result.error_messages))) - + messagebox.setWindowModality(Qt.NonModal) + details: List = [] + for res in success: + details.append( + self.tr("{} was imported successfully").format(self.core.get_game(res.app_name).app_title) + ) + for res in failure: + details.append( + self.tr("Failed: {}").format(res.message) + ) + for res in errored: + details.append( + self.tr("Error: {}").format(res.message) + ) + messagebox.setDetailedText("\n".join(details)) + messagebox.show() def import_progress(self, progress: int): pass diff --git a/rare/ui/components/tabs/games/import_sync/import_group.py b/rare/ui/components/tabs/games/import_sync/import_group.py index f1e8222e..47fa08c2 100644 --- a/rare/ui/components/tabs/games/import_sync/import_group.py +++ b/rare/ui/components/tabs/games/import_sync/import_group.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'rare/ui/components/tabs/games/import_sync/import_group.ui' # -# Created by: PyQt5 UI code generator 5.15.6 +# Created by: PyQt5 UI code generator 5.15.7 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -14,31 +14,34 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_ImportGroup(object): def setupUi(self, ImportGroup): ImportGroup.setObjectName("ImportGroup") - ImportGroup.resize(501, 154) + ImportGroup.resize(501, 136) ImportGroup.setWindowTitle("ImportGroup") ImportGroup.setWindowFilePath("") - self.import_layout = QtWidgets.QFormLayout(ImportGroup) - self.import_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.import_layout.setObjectName("import_layout") + self.formLayout = QtWidgets.QFormLayout(ImportGroup) + self.formLayout.setObjectName("formLayout") self.path_edit_label = QtWidgets.QLabel(ImportGroup) self.path_edit_label.setObjectName("path_edit_label") - self.import_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.path_edit_label) + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.path_edit_label) self.path_edit_layout = QtWidgets.QHBoxLayout() self.path_edit_layout.setObjectName("path_edit_layout") - self.import_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.path_edit_layout) + self.formLayout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.path_edit_layout) self.app_name_label = QtWidgets.QLabel(ImportGroup) self.app_name_label.setObjectName("app_name_label") - self.import_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.app_name_label) + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.app_name_label) self.app_name_layout = QtWidgets.QHBoxLayout() self.app_name_layout.setObjectName("app_name_layout") - self.import_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.app_name_layout) + self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.app_name_layout) self.import_folder_label = QtWidgets.QLabel(ImportGroup) self.import_folder_label.setObjectName("import_folder_label") - self.import_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.import_folder_label) - self.info_label = QtWidgets.QLabel(ImportGroup) - self.info_label.setText("") - self.info_label.setObjectName("info_label") - self.import_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.info_label) + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.import_folder_label) + self.import_folder_check = QtWidgets.QCheckBox(ImportGroup) + font = QtGui.QFont() + font.setItalic(True) + self.import_folder_check.setFont(font) + self.import_folder_check.setObjectName("import_folder_check") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.import_folder_check) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") self.import_button = QtWidgets.QPushButton(ImportGroup) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -46,13 +49,12 @@ class Ui_ImportGroup(object): sizePolicy.setHeightForWidth(self.import_button.sizePolicy().hasHeightForWidth()) self.import_button.setSizePolicy(sizePolicy) self.import_button.setObjectName("import_button") - self.import_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.import_button) - self.import_folder_check = QtWidgets.QCheckBox(ImportGroup) - font = QtGui.QFont() - font.setItalic(True) - self.import_folder_check.setFont(font) - self.import_folder_check.setObjectName("import_folder_check") - self.import_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.import_folder_check) + self.horizontalLayout.addWidget(self.import_button) + self.info_label = QtWidgets.QLabel(ImportGroup) + self.info_label.setText("") + self.info_label.setObjectName("info_label") + self.horizontalLayout.addWidget(self.info_label) + self.formLayout.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout) self.retranslateUi(ImportGroup) QtCore.QMetaObject.connectSlotsByName(ImportGroup) @@ -63,8 +65,8 @@ class Ui_ImportGroup(object): self.path_edit_label.setText(_translate("ImportGroup", "Installation path")) self.app_name_label.setText(_translate("ImportGroup", "Override app name")) self.import_folder_label.setText(_translate("ImportGroup", "Import all folders")) - self.import_button.setText(_translate("ImportGroup", "Import Game")) self.import_folder_check.setText(_translate("ImportGroup", "Scan the installation path for game folders and import them")) + self.import_button.setText(_translate("ImportGroup", "Import Game")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/games/import_sync/import_group.ui b/rare/ui/components/tabs/games/import_sync/import_group.ui index 0545451f..1f149d1f 100644 --- a/rare/ui/components/tabs/games/import_sync/import_group.ui +++ b/rare/ui/components/tabs/games/import_sync/import_group.ui @@ -7,7 +7,7 @@ 0 0 501 - 154 + 136 @@ -19,10 +19,7 @@ Import EGL game from a directory - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + @@ -50,26 +47,6 @@ - - - - - - - - - - - - 0 - 0 - - - - Import Game - - - @@ -82,6 +59,30 @@ + + + + + + + 0 + 0 + + + + Import Game + + + + + + + + + + + +