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