From ecc1bd8d5c633769bb835f0ae51989cbff0ae1de Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 18 Feb 2023 14:33:42 +0200 Subject: [PATCH] IndicatorLineEdit: Run edit callback function asynchronously Execute the edit callback function in a thread. By executing it in a thread we don't have to wait for longer validation procedures to finish to continue updating the UI. This is most notable in the MoveGamePopUp which is heavy on disk IO. Because we cannot use special text formatting in a thread, the indicator messages have been reworked while also becoming extensible. A dictionary of extended reasons can be specified through the `IndicatorLineEdit.extend_reasons()` method. The dictionary has to follow the following format ``` python { MyIndicatorReasons.REASON: self.tr("Reason message") MyIndicatorReasons.OTHER_REASON: self.tr("Other reason message") } ``` In the above example `MyIndicatorReasons` is a subclass of `IndicatorReasons` which should be specified as follows ``` python MyIndicatorReasons(IndicatorReasons): REASON = auto() OTHER_REASON = auto() ``` --- rare/components/dialogs/install_dialog.py | 6 +- .../components/dialogs/login/browser_login.py | 10 +- .../tabs/games/game_info/game_settings.py | 6 +- .../tabs/games/game_info/move_game.py | 57 ++++--- .../tabs/games/integrations/egl_sync_group.py | 12 +- .../tabs/games/integrations/import_group.py | 20 +-- rare/components/tabs/settings/legendary.py | 13 +- .../components/tabs/settings/widgets/linux.py | 6 +- .../tabs/settings/widgets/pre_launch.py | 10 +- .../tabs/settings/widgets/proton.py | 8 +- rare/utils/extra_widgets.py | 156 +++++++++++++----- rare/utils/misc.py | 4 +- 12 files changed, 204 insertions(+), 104 deletions(-) diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index d2fca89d..e0d4abc6 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -15,7 +15,7 @@ from rare.shared.workers.install_info import InstallInfoWorker from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced from rare.utils import config_helper -from rare.utils.extra_widgets import PathEdit +from rare.utils.extra_widgets import PathEdit, IndicatorReasonsCommon from rare.utils.misc import get_size from rare.widgets.collapsible_widget import CollapsibleFrame @@ -253,11 +253,11 @@ class InstallDialog(QDialog): self.get_options() self.get_download_info() - def option_changed(self, path) -> Tuple[bool, str, str]: + def option_changed(self, path) -> Tuple[bool, str, int]: self.options_changed = True self.ui.install_button.setEnabled(False) self.ui.verify_button.setEnabled(not self.worker_running) - return True, path, "" + return True, path, IndicatorReasonsCommon.VALID def non_reload_option_changed(self, option: str): if option == "download_only": diff --git a/rare/components/dialogs/login/browser_login.py b/rare/components/dialogs/login/browser_login.py index 6be59419..5fa223c0 100644 --- a/rare/components/dialogs/login/browser_login.py +++ b/rare/components/dialogs/login/browser_login.py @@ -9,7 +9,7 @@ from legendary.core import LegendaryCore from legendary.utils import webview_login from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin -from rare.utils.extra_widgets import IndicatorLineEdit +from rare.utils.extra_widgets import IndicatorLineEdit, IndicatorReasonsCommon from rare.utils.misc import icon logger = getLogger("BrowserLogin") @@ -49,19 +49,19 @@ class BrowserLogin(QFrame): return self.sid_edit.is_valid @staticmethod - def text_changed(text) -> Tuple[bool, str, str]: + def text_changed(text) -> Tuple[bool, str, int]: if text: text = text.strip() if text.startswith("{") and text.endswith("}"): try: text = json.loads(text).get("authorizationCode") except json.JSONDecodeError: - return False, text, IndicatorLineEdit.reasons.wrong_format + return False, text, IndicatorReasonsCommon.WRONG_FORMAT elif '"' in text: text = text.strip('"') - return len(text) == 32, text, IndicatorLineEdit.reasons.wrong_format + return len(text) == 32, text, IndicatorReasonsCommon.WRONG_FORMAT else: - return False, text, "" + return False, text, IndicatorReasonsCommon.VALID def do_login(self): self.ui.status_label.setText(self.tr("Logging in...")) diff --git a/rare/components/tabs/games/game_info/game_settings.py b/rare/components/tabs/games/game_info/game_settings.py index a18db95e..d70c8367 100644 --- a/rare/components/tabs/games/game_info/game_settings.py +++ b/rare/components/tabs/games/game_info/game_settings.py @@ -11,7 +11,7 @@ from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings from rare.models.game import RareGame from rare.shared.workers.wine_resolver import WineResolver from rare.utils import config_helper -from rare.utils.extra_widgets import PathEdit +from rare.utils.extra_widgets import PathEdit, IndicatorReasonsCommon from rare.utils.misc import icon, get_raw_save_path logger = getLogger("GameSettings") @@ -32,9 +32,9 @@ class GameSettings(DefaultGameSettings): "", file_type=QFileDialog.DirectoryOnly, placeholder=self.tr("Cloud save path"), - edit_func=lambda text: (True, text, None) + edit_func=lambda text: (True, text, IndicatorReasonsCommon.VALID) if os.path.exists(text) - else (False, text, PathEdit.reasons.dir_not_exist), + else (False, text, IndicatorReasonsCommon.DIR_NOT_EXISTS), save_func=self.save_save_path, ) self.cloud_layout.addRow(QLabel(self.tr("Save path")), self.cloud_save_path_edit) diff --git a/rare/components/tabs/games/game_info/move_game.py b/rare/components/tabs/games/game_info/move_game.py index 2b0bb7f4..cfcc3624 100644 --- a/rare/components/tabs/games/game_info/move_game.py +++ b/rare/components/tabs/games/game_info/move_game.py @@ -1,5 +1,6 @@ import os import shutil +from enum import auto from logging import getLogger from typing import Tuple, Optional @@ -7,24 +8,42 @@ from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog from rare.models.game import RareGame -from rare.shared import LegendaryCoreSingleton -from rare.utils.extra_widgets import PathEdit +from rare.shared import RareCore +from rare.utils.extra_widgets import PathEdit, IndicatorReasons, IndicatorReasonsCommon from rare.utils.misc import get_size from rare.widgets.elide_label import ElideLabel logger = getLogger("MoveGame") +class MovePathEditReasons(IndicatorReasons): + DST_MISSING = auto() + NO_WRITE_PERM = auto() + SAME_DIR = auto() + DST_IN_SRC = auto() + NESTED_DIR = auto() + NO_SPACE = auto() + + class MoveGamePopUp(QWidget): move_clicked = pyqtSignal(str) browse_done = pyqtSignal() def __init__(self, parent=None): super(MoveGamePopUp, self).__init__(parent=parent) - self.core = LegendaryCoreSingleton() + self.rcore = RareCore.instance() + self.core = RareCore.instance().core() self.rgame: Optional[RareGame] = None - self.path_edit = PathEdit("", QFileDialog.Directory, edit_func=self.path_edit_cb) + self.path_edit = PathEdit("", QFileDialog.Directory, edit_func=self.path_edit_callback) + self.path_edit.extend_reasons({ + MovePathEditReasons.DST_MISSING: self.tr("You need to provide a directory."), + MovePathEditReasons.NO_WRITE_PERM: self.tr("No write permission on destination."), + MovePathEditReasons.SAME_DIR: self.tr("Same directory or subdirectory selected."), + MovePathEditReasons.DST_IN_SRC: self.tr("Destination is inside source directory"), + MovePathEditReasons.NESTED_DIR: self.tr("Game install directories cannot be nested."), + MovePathEditReasons.NO_SPACE: self.tr("Not enough space available on drive."), + }) self.path_edit.path_select.clicked.connect(self.browse_done) self.button = QPushButton(self.tr("Move")) @@ -64,23 +83,23 @@ class MoveGamePopUp(QWidget): self.path_edit.setText(str()) self.path_edit.setText(text) - def path_edit_cb(self, path: str): + def path_edit_callback(self, path: str) -> Tuple[bool, str, int]: self.button.setEnabled(True) self.warn_label.setHidden(False) - def helper_func(reason: str) -> Tuple[bool, str, str]: + def helper_func(reason: int) -> Tuple[bool, str, int]: self.button.setEnabled(False) - return False, path, self.tr(reason) + return False, path, reason if not self.rgame.install_path or not path: - return helper_func("You need to provide a directory.") + return helper_func(MovePathEditReasons.DST_MISSING) src_path = os.path.realpath(self.rgame.install_path) dst_path = os.path.realpath(path) dst_install_path = os.path.realpath(os.path.join(dst_path, os.path.basename(src_path))) if not os.path.isdir(dst_path): - return helper_func("Directory doesn't exist or file selected.") + return helper_func(IndicatorReasonsCommon.DIR_NOT_EXISTS) # Get free space on drive and size of game folder _, _, free_space = shutil.disk_usage(dst_path) @@ -97,20 +116,20 @@ class MoveGamePopUp(QWidget): self.avail_space.setText(get_size(free_space)) if not os.access(path, os.W_OK) or not os.access(self.rgame.install_path, os.W_OK): - return helper_func("No write permission on destination path/current install path.") + return helper_func(MovePathEditReasons.NO_WRITE_PERM) if src_path == dst_path or src_path == dst_install_path: - return helper_func("Same directory or parent directory selected.") + return helper_func(MovePathEditReasons.SAME_DIR) if str(src_path) in str(dst_path): - return helper_func("You can't select a directory that is inside the current install path.") + return helper_func(MovePathEditReasons.DST_IN_SRC) if str(dst_install_path) in str(src_path): - return helper_func("You can't select a directory which contains the game installation.") + return helper_func(MovePathEditReasons.DST_IN_SRC) - for game in self.core.get_installed_list(): - if game.install_path in path: - return helper_func("Game installations cannot be nested due to unintended sideeffects.") + for rgame in self.rcore.installed_games: + if not rgame.is_non_asset and rgame.install_path in path: + return helper_func(MovePathEditReasons.NESTED_DIR) is_existing_dir = is_game_dir(src_path, dst_install_path) @@ -123,16 +142,16 @@ class MoveGamePopUp(QWidget): self.warn_label.setHidden(False) if free_space <= source_size and not is_existing_dir: - return helper_func("Not enough space available on drive.") + return helper_func(MovePathEditReasons.NO_SPACE) # Fallback self.button.setEnabled(True) - return True, path, str() + return True, path, IndicatorReasonsCommon.VALID @pyqtSlot() def __update_widget(self): """ React to state updates from RareGame """ - if not self.rgame.is_installed and not self.rgame.is_non_asset: + if not self.rgame.is_installed or self.rgame.is_non_asset: self.setDisabled(True) return # FIXME: Make edit_func lighter instead of blocking signals diff --git a/rare/components/tabs/games/integrations/egl_sync_group.py b/rare/components/tabs/games/integrations/egl_sync_group.py index 4cc30cda..d7b5fdc2 100644 --- a/rare/components/tabs/games/integrations/egl_sync_group.py +++ b/rare/components/tabs/games/integrations/egl_sync_group.py @@ -15,7 +15,7 @@ from rare.shared import RareCore from rare.shared.workers.wine_resolver import WineResolver from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup -from rare.utils.extra_widgets import PathEdit +from rare.utils.extra_widgets import PathEdit, IndicatorReasonsCommon logger = getLogger("EGLSync") @@ -92,9 +92,9 @@ class EGLSyncGroup(QGroupBox): self.egl_path_edit.setText(path) @staticmethod - def egl_path_edit_edit_cb(path) -> Tuple[bool, str, str]: + def egl_path_edit_edit_cb(path) -> Tuple[bool, str, int]: if not path: - return True, path, "" + return True, path, IndicatorReasonsCommon.VALID if os.path.exists(os.path.join(path, "system.reg")) and os.path.exists( os.path.join(path, "dosdevices/c:") ): @@ -108,10 +108,10 @@ class EGLSyncGroup(QGroupBox): "ProgramData/Epic/EpicGamesLauncher/Data/Manifests" ): # lower() might or might not be needed in the check - return False, path, PathEdit.reasons.wrong_path + return False, path, IndicatorReasonsCommon.WRONG_FORMAT if os.path.exists(path): - return True, path, "" - return False, path, PathEdit.reasons.dir_not_exist + return True, path, IndicatorReasonsCommon.VALID + return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS def egl_path_edit_save_cb(self, path): if not path or not os.path.exists(path): diff --git a/rare/components/tabs/games/integrations/import_group.py b/rare/components/tabs/games/integrations/import_group.py index bc644d95..01affd51 100644 --- a/rare/components/tabs/games/integrations/import_group.py +++ b/rare/components/tabs/games/integrations/import_group.py @@ -17,7 +17,7 @@ from rare.lgndr.glue.arguments import LgndrImportGameArgs from rare.lgndr.glue.monkeys import LgndrIndirectStatus from rare.shared import RareCore from rare.ui.components.tabs.games.integrations.import_group import Ui_ImportGroup -from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit +from rare.utils.extra_widgets import IndicatorLineEdit, IndicatorReasonsCommon, PathEdit from rare.widgets.elide_label import ElideLabel logger = getLogger("Import") @@ -212,15 +212,15 @@ class ImportGroup(QGroupBox): self.threadpool = QThreadPool.globalInstance() - def path_edit_callback(self, path) -> Tuple[bool, str, str]: + def path_edit_callback(self, path) -> Tuple[bool, str, int]: if os.path.exists(path): if os.path.exists(os.path.join(path, ".egstore")): - return True, path, "" + return True, path, IndicatorReasonsCommon.VALID elif os.path.basename(path) in self.install_dir_list: - return True, path, "" + return True, path, IndicatorReasonsCommon.VALID else: - return False, path, PathEdit.reasons.dir_not_exist - return False, path, "" + return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS + return False, path, IndicatorReasonsCommon.UNDEFINED @pyqtSlot(str) def path_changed(self, path: str): @@ -231,13 +231,13 @@ class ImportGroup(QGroupBox): else: self.app_name_edit.setText("") - def app_name_edit_callback(self, text) -> Tuple[bool, str, str]: + def app_name_edit_callback(self, text) -> Tuple[bool, str, int]: if not text: - return False, text, "" + return False, text, IndicatorReasonsCommon.UNDEFINED if text in self.app_name_list: - return True, text, "" + return True, text, IndicatorReasonsCommon.VALID else: - return False, text, IndicatorLineEdit.reasons.game_not_installed + return False, text, IndicatorReasonsCommon.NOT_INSTALLED @pyqtSlot(str) def app_name_changed(self, app_name: str): diff --git a/rare/components/tabs/settings/legendary.py b/rare/components/tabs/settings/legendary.py index 996f0623..d2cd1185 100644 --- a/rare/components/tabs/settings/legendary.py +++ b/rare/components/tabs/settings/legendary.py @@ -3,12 +3,12 @@ import re from logging import getLogger from typing import Tuple -from PyQt5.QtCore import Qt, QRunnable, QObject, pyqtSignal, QThreadPool, QSettings +from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, QSettings from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox from rare.shared import LegendaryCoreSingleton from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings -from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit +from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit, IndicatorReasonsCommon from rare.utils.misc import get_size logger = getLogger("LegendarySettings") @@ -102,18 +102,17 @@ class LegendarySettings(QWidget, Ui_LegendarySettings): QThreadPool.globalInstance().start(worker) @staticmethod - def locale_edit_cb(text: str) -> Tuple[bool, str, str]: + def locale_edit_cb(text: str) -> Tuple[bool, str, int]: if text: if re.match("^[a-zA-Z]{2,3}[-_][a-zA-Z]{2,3}$", text): language, country = text.replace("_", "-").split("-") text = "-".join([language.lower(), country.upper()]) if bool(re.match("^[a-z]{2,3}-[A-Z]{2,3}$", text)): - return True, text, "" + return True, text, IndicatorReasonsCommon.VALID else: - return False, text, IndicatorLineEdit.reasons.wrong_format - + return False, text, IndicatorReasonsCommon.WRONG_FORMAT else: - return True, text, "" + return True, text, IndicatorReasonsCommon.VALID def locale_save_cb(self, text: str): if text: diff --git a/rare/components/tabs/settings/widgets/linux.py b/rare/components/tabs/settings/widgets/linux.py index 698ef6f2..6a22ebfb 100644 --- a/rare/components/tabs/settings/widgets/linux.py +++ b/rare/components/tabs/settings/widgets/linux.py @@ -9,7 +9,7 @@ from rare.components.tabs.settings.widgets.mangohud import MangoHudSettings from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton from rare.ui.components.tabs.settings.linux import Ui_LinuxSettings -from rare.utils.extra_widgets import PathEdit +from rare.utils.extra_widgets import PathEdit, IndicatorReasonsCommon logger = getLogger("LinuxSettings") @@ -28,7 +28,7 @@ class LinuxSettings(QWidget, Ui_LinuxSettings): self.wine_prefix = PathEdit( self.load_prefix(), file_type=QFileDialog.DirectoryOnly, - edit_func=lambda path: (os.path.isdir(path) or not path, path, PathEdit.reasons.dir_not_exist), + edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS), save_func=self.save_prefix, ) self.prefix_layout.addWidget(self.wine_prefix) @@ -38,7 +38,7 @@ class LinuxSettings(QWidget, Ui_LinuxSettings): self.load_setting(self.name, "wine_executable"), file_type=QFileDialog.ExistingFile, name_filter="Wine executable (wine wine64)", - edit_func=lambda text: (os.path.exists(text) or not text, text, PathEdit.reasons.dir_not_exist), + edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS), save_func=lambda text: self.save_setting( text, section=self.name, setting="wine_executable" ), diff --git a/rare/components/tabs/settings/widgets/pre_launch.py b/rare/components/tabs/settings/widgets/pre_launch.py index 8b016be9..2fc78f25 100644 --- a/rare/components/tabs/settings/widgets/pre_launch.py +++ b/rare/components/tabs/settings/widgets/pre_launch.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog from rare.shared import LegendaryCoreSingleton from rare.utils import config_helper -from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit +from rare.utils.extra_widgets import PathEdit, IndicatorReasonsCommon class PreLaunchSettings(QHBoxLayout): @@ -28,14 +28,14 @@ class PreLaunchSettings(QHBoxLayout): self.layout().addWidget(self.wait_check) self.wait_check.stateChanged.connect(self.save_wait_finish) - def edit_command(self, text: str) -> Tuple[bool, str, str]: + def edit_command(self, text: str) -> Tuple[bool, str, int]: if not text.strip(): - return True, text, "" + return True, text, IndicatorReasonsCommon.VALID if not os.path.isfile(text.split()[0]) and not shutil.which(text.split()[0]): - return False, text, IndicatorLineEdit.reasons.file_not_exist + return False, text, IndicatorReasonsCommon.FILE_NOT_EXISTS else: - return True, text, "" + return True, text, IndicatorReasonsCommon.VALID def save_pre_launch_command(self, text): if text: diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py index 2f55fed0..d140bce7 100644 --- a/rare/components/tabs/settings/widgets/proton.py +++ b/rare/components/tabs/settings/widgets/proton.py @@ -9,7 +9,7 @@ from rare.components.tabs.settings import LinuxSettings from .wrapper import WrapperSettings from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings from rare.utils import config_helper -from rare.utils.extra_widgets import PathEdit +from rare.utils.extra_widgets import PathEdit, IndicatorReasonsCommon from rare.shared import LegendaryCoreSingleton logger = getLogger("Proton") @@ -103,12 +103,12 @@ class ProtonSettings(QGroupBox, Ui_ProtonSettings): config_helper.save_config() - def proton_prefix_edit(self, text: str) -> Tuple[bool, str, str]: + def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]: if not text: text = os.path.expanduser("~/.proton") - return True, text, "" + return True, text, IndicatorReasonsCommon.VALID parent_dir = os.path.dirname(text) - return os.path.exists(parent_dir), text, PathEdit.reasons.dir_not_exist + return os.path.exists(parent_dir), text, IndicatorReasonsCommon.DIR_NOT_EXISTS def proton_prefix_save(self, text: str): if not self.changeable: diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 1f219782..54b98837 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -1,6 +1,7 @@ import os +from enum import IntEnum from logging import getLogger -from typing import Callable, Tuple, Optional +from typing import Callable, Tuple, Optional, Dict from PyQt5.QtCore import ( Qt, @@ -10,6 +11,10 @@ from PyQt5.QtCore import ( QPoint, pyqtSignal, QFileInfo, + QRunnable, + QObject, + QThreadPool, + pyqtSlot, ) from PyQt5.QtGui import QMovie, QPixmap, QFontMetrics, QImage from PyQt5.QtWidgets import ( @@ -34,34 +39,94 @@ from PyQt5.QtWidgets import ( QScrollArea, ) +from rare.utils.misc import icon as qta_icon from rare.utils.paths import tmp_dir from rare.utils.qt_requests import QtRequestManager -from rare.utils.misc import icon as qta_icon logger = getLogger("ExtraWidgets") -class IndicatorReasons: - dir_not_empty = QCoreApplication.translate("IndicatorReasons", "Directory is not empty") - wrong_format = QCoreApplication.translate("IndicatorReasons", "Given text has wrong format") - game_not_installed = QCoreApplication.translate( - "IndicatorReasons", "Game is not installed or does not exist" - ) - dir_not_exist = QCoreApplication.translate("IndicatorReasons", "Directory does not exist") - file_not_exist = QCoreApplication.translate("IndicatorReasons", "File does not exist") - wrong_path = QCoreApplication.translate("IndicatorReasons", "Wrong Directory") +class IndicatorReasons(IntEnum): + """ + Empty enumeration with auto-generated enumeration values. + Extend this class per-case to implement dedicated message types. + Types should be assigned using `auto()` from enum + + example: + MyReasons(IndicatorReasons): + MY_REASON = auto() + """ + @staticmethod + def _generate_next_value_(name, start, count, last_values): + """generate consecutive automatic numbers starting from zero""" + start = len(IndicatorReasonsCommon) + return IntEnum._generate_next_value_(name, start, count, last_values) + + +class IndicatorReasonsCommon(IndicatorReasons): + VALID = 0 + UNDEFINED = 1 + WRONG_FORMAT = 2 + WRONG_PATH = 3 + DIR_NOT_EMPTY = 4 + DIR_NOT_EXISTS = 5 + FILE_NOT_EXISTS = 6 + NOT_INSTALLED = 7 + + +class IndicatorReasonsText(QObject): + def __init__(self, parent=None): + super(IndicatorReasonsText, self).__init__(parent=parent) + self.__text = { + IndicatorReasonsCommon.VALID: self.tr("Ok!"), + IndicatorReasonsCommon.UNDEFINED: self.tr("Unknown error occured"), + IndicatorReasonsCommon.WRONG_FORMAT: self.tr("Wrong format"), + IndicatorReasonsCommon.WRONG_PATH: self.tr("Wrong directory"), + IndicatorReasonsCommon.DIR_NOT_EMPTY: self.tr("Directory is not empty"), + IndicatorReasonsCommon.DIR_NOT_EXISTS: self.tr("Directory does not exist"), + IndicatorReasonsCommon.FILE_NOT_EXISTS: self.tr("File does not exist"), + IndicatorReasonsCommon.NOT_INSTALLED: self.tr("Game is not installed or does not exist"), + } + + def __getitem__(self, item: int) -> str: + return self.__text[item] + + def __setitem__(self, key: int, value: str): + self.__text[key] = value + + def extend(self, reasons: Dict): + for k in self.__text.keys(): + if k in reasons.keys(): + raise RuntimeError(f"{reasons} contains existing values") + self.__text.update(reasons) + + +class EditFuncRunnable(QRunnable): + class Signals(QObject): + result = pyqtSignal(bool, str, int) + + def __init__(self, func: Callable[[str], Tuple[bool, str, int]], args: str): + super(EditFuncRunnable, self).__init__() + self.setAutoDelete(True) + self.signals = EditFuncRunnable.Signals() + self.func = func + self.args = args + + def run(self): + o0, o1, o2 = self.func(self.args) + self.signals.result.emit(o0, o1, o2) + self.signals.deleteLater() class IndicatorLineEdit(QWidget): textChanged = pyqtSignal(str) - reasons = IndicatorReasons() def __init__( self, text: str = "", placeholder: str = "", completer: QCompleter = None, - edit_func: Callable[[str], Tuple[bool, str, str]] = None, + edit_func: Callable[[str], Tuple[bool, str, int]] = None, save_func: Callable[[str], None] = None, horiz_policy: QSizePolicy.Policy = QSizePolicy.Expanding, parent=None, @@ -99,6 +164,12 @@ class IndicatorLineEdit(QWidget): _translate = QCoreApplication.instance().translate self.line_edit.setPlaceholderText(_translate(self.__class__.__name__, "Default")) + self.__reasons = IndicatorReasonsText(self) + + self.__threadpool = QThreadPool(self) + self.__threadpool.setMaxThreadCount(1) + self.__thread: Optional[EditFuncRunnable] = None + self.is_valid = False self.edit_func = edit_func self.save_func = save_func @@ -125,26 +196,37 @@ class IndicatorLineEdit(QWidget): self.hint_label.setFrameRect(self.line_edit.rect()) self.hint_label.setText(text) - def __indicator(self, res, reason=None): - color = "green" if res else "red" + def extend_reasons(self, reasons: Dict): + self.__reasons.extend(reasons) + + def __indicator(self, valid, reason: int = 0): + color = "green" if valid else "red" self.indicator_label.setPixmap(qta_icon("ei.info-circle", color=color).pixmap(16, 16)) - if reason: - self.indicator_label.setToolTip(reason) + if not valid: + self.indicator_label.setToolTip(self.__reasons[reason]) else: - self.indicator_label.setToolTip("") + self.indicator_label.setToolTip(self.__reasons[IndicatorReasonsCommon.VALID]) + + @pyqtSlot(bool, str, int) + def __edit_handler(self, is_valid: bool, text: str, reason: int): + self.__thread = None + self.line_edit.blockSignals(True) + if text != self.line_edit.text(): + self.line_edit.setText(text) + self.line_edit.blockSignals(False) + self.__indicator(is_valid, reason) + if is_valid: + self.__save(text) + self.is_valid = is_valid + self.textChanged.emit(text) def __edit(self, text): if self.edit_func is not None: - self.line_edit.blockSignals(True) - - self.is_valid, text, reason = self.edit_func(text) - if text != self.line_edit.text(): - self.line_edit.setText(text) - self.line_edit.blockSignals(False) - self.__indicator(self.is_valid, reason) - if self.is_valid: - self.__save(text) - self.textChanged.emit(text) + if self.__thread is not None: + self.__thread.signals.result.disconnect(self.__edit_handler) + self.__thread = EditFuncRunnable(self.edit_func, text) + self.__thread.signals.result.connect(self.__edit_handler) + self.__threadpool.start(self.__thread) def __save(self, text): if self.save_func is not None: @@ -166,8 +248,8 @@ class PathEditIconProvider(QFileIconProvider): def __init__(self): super(PathEditIconProvider, self).__init__() - self.icon_types = dict() - for idx, (icn, fallback) in enumerate(self.icons): + self.icon_types = {} + for idx, (icn, fallback) in enumerate(PathEditIconProvider.icons): self.icon_types.update({idx - 1: qta_icon(icn, fallback, color="#eeeeee")}) def icon(self, info_type): @@ -192,7 +274,7 @@ class PathEdit(IndicatorLineEdit): type_filter: str = "", name_filter: str = "", placeholder: str = "", - edit_func: Callable[[str], Tuple[bool, str, str]] = None, + edit_func: Callable[[str], Tuple[bool, str, int]] = None, save_func: Callable[[str], None] = None, horiz_policy: QSizePolicy.Policy = QSizePolicy.Expanding, parent=None, @@ -230,8 +312,7 @@ class PathEdit(IndicatorLineEdit): layout = self.layout() layout.addWidget(self.path_select) - _translate = QCoreApplication.instance().translate - self.path_select.setText(_translate("PathEdit", "Browse...")) + self.path_select.setText(self.tr("Browse...")) self.type_filter = type_filter self.name_filter = name_filter @@ -254,11 +335,12 @@ class PathEdit(IndicatorLineEdit): self.line_edit.setText(names[0]) self.compl_model.setRootPath(names[0]) - def __wrap_edit_function(self, edit_function: Callable[[str], Tuple[bool, str, str]]): - if edit_function: - return lambda text: edit_function(os.path.expanduser(text) if text.startswith("~") else text) + @staticmethod + def __wrap_edit_function(func: Callable[[str], Tuple[bool, str, int]]): + if func: + return lambda text: func(os.path.expanduser(text) if text.startswith("~") else text) else: - return edit_function + return func class SideTabBar(QTabBar): diff --git a/rare/utils/misc.py b/rare/utils/misc.py index 0a297f04..59157151 100644 --- a/rare/utils/misc.py +++ b/rare/utils/misc.py @@ -144,9 +144,9 @@ def get_latest_version(): def get_size(b: Union[int, float]) -> str: - for s in ["", "K", "M", "G", "T", "P", "E"]: + for s in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"]: if b < 1024: - return f"{b:.2f} {s}iB" + return f"{b:.2f} {s}B" b /= 1024