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() ```
This commit is contained in:
parent
7e4ded70b5
commit
ecc1bd8d5c
12 changed files with 204 additions and 104 deletions
|
@ -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 import Ui_InstallDialog
|
||||||
from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
|
from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
|
||||||
from rare.utils import config_helper
|
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.utils.misc import get_size
|
||||||
from rare.widgets.collapsible_widget import CollapsibleFrame
|
from rare.widgets.collapsible_widget import CollapsibleFrame
|
||||||
|
|
||||||
|
@ -253,11 +253,11 @@ class InstallDialog(QDialog):
|
||||||
self.get_options()
|
self.get_options()
|
||||||
self.get_download_info()
|
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.options_changed = True
|
||||||
self.ui.install_button.setEnabled(False)
|
self.ui.install_button.setEnabled(False)
|
||||||
self.ui.verify_button.setEnabled(not self.worker_running)
|
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):
|
def non_reload_option_changed(self, option: str):
|
||||||
if option == "download_only":
|
if option == "download_only":
|
||||||
|
|
|
@ -9,7 +9,7 @@ from legendary.core import LegendaryCore
|
||||||
from legendary.utils import webview_login
|
from legendary.utils import webview_login
|
||||||
|
|
||||||
from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin
|
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
|
from rare.utils.misc import icon
|
||||||
|
|
||||||
logger = getLogger("BrowserLogin")
|
logger = getLogger("BrowserLogin")
|
||||||
|
@ -49,19 +49,19 @@ class BrowserLogin(QFrame):
|
||||||
return self.sid_edit.is_valid
|
return self.sid_edit.is_valid
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def text_changed(text) -> Tuple[bool, str, str]:
|
def text_changed(text) -> Tuple[bool, str, int]:
|
||||||
if text:
|
if text:
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
if text.startswith("{") and text.endswith("}"):
|
if text.startswith("{") and text.endswith("}"):
|
||||||
try:
|
try:
|
||||||
text = json.loads(text).get("authorizationCode")
|
text = json.loads(text).get("authorizationCode")
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return False, text, IndicatorLineEdit.reasons.wrong_format
|
return False, text, IndicatorReasonsCommon.WRONG_FORMAT
|
||||||
elif '"' in text:
|
elif '"' in text:
|
||||||
text = text.strip('"')
|
text = text.strip('"')
|
||||||
return len(text) == 32, text, IndicatorLineEdit.reasons.wrong_format
|
return len(text) == 32, text, IndicatorReasonsCommon.WRONG_FORMAT
|
||||||
else:
|
else:
|
||||||
return False, text, ""
|
return False, text, IndicatorReasonsCommon.VALID
|
||||||
|
|
||||||
def do_login(self):
|
def do_login(self):
|
||||||
self.ui.status_label.setText(self.tr("Logging in..."))
|
self.ui.status_label.setText(self.tr("Logging in..."))
|
||||||
|
|
|
@ -11,7 +11,7 @@ from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
|
||||||
from rare.models.game import RareGame
|
from rare.models.game import RareGame
|
||||||
from rare.shared.workers.wine_resolver import WineResolver
|
from rare.shared.workers.wine_resolver import WineResolver
|
||||||
from rare.utils import config_helper
|
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
|
from rare.utils.misc import icon, get_raw_save_path
|
||||||
|
|
||||||
logger = getLogger("GameSettings")
|
logger = getLogger("GameSettings")
|
||||||
|
@ -32,9 +32,9 @@ class GameSettings(DefaultGameSettings):
|
||||||
"",
|
"",
|
||||||
file_type=QFileDialog.DirectoryOnly,
|
file_type=QFileDialog.DirectoryOnly,
|
||||||
placeholder=self.tr("Cloud save path"),
|
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)
|
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,
|
save_func=self.save_save_path,
|
||||||
)
|
)
|
||||||
self.cloud_layout.addRow(QLabel(self.tr("Save path")), self.cloud_save_path_edit)
|
self.cloud_layout.addRow(QLabel(self.tr("Save path")), self.cloud_save_path_edit)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
from enum import auto
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Tuple, Optional
|
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 PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog
|
||||||
|
|
||||||
from rare.models.game import RareGame
|
from rare.models.game import RareGame
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import RareCore
|
||||||
from rare.utils.extra_widgets import PathEdit
|
from rare.utils.extra_widgets import PathEdit, IndicatorReasons, IndicatorReasonsCommon
|
||||||
from rare.utils.misc import get_size
|
from rare.utils.misc import get_size
|
||||||
from rare.widgets.elide_label import ElideLabel
|
from rare.widgets.elide_label import ElideLabel
|
||||||
|
|
||||||
logger = getLogger("MoveGame")
|
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):
|
class MoveGamePopUp(QWidget):
|
||||||
move_clicked = pyqtSignal(str)
|
move_clicked = pyqtSignal(str)
|
||||||
browse_done = pyqtSignal()
|
browse_done = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(MoveGamePopUp, self).__init__(parent=parent)
|
super(MoveGamePopUp, self).__init__(parent=parent)
|
||||||
self.core = LegendaryCoreSingleton()
|
self.rcore = RareCore.instance()
|
||||||
|
self.core = RareCore.instance().core()
|
||||||
self.rgame: Optional[RareGame] = None
|
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.path_edit.path_select.clicked.connect(self.browse_done)
|
||||||
|
|
||||||
self.button = QPushButton(self.tr("Move"))
|
self.button = QPushButton(self.tr("Move"))
|
||||||
|
@ -64,23 +83,23 @@ class MoveGamePopUp(QWidget):
|
||||||
self.path_edit.setText(str())
|
self.path_edit.setText(str())
|
||||||
self.path_edit.setText(text)
|
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.button.setEnabled(True)
|
||||||
self.warn_label.setHidden(False)
|
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)
|
self.button.setEnabled(False)
|
||||||
return False, path, self.tr(reason)
|
return False, path, reason
|
||||||
|
|
||||||
if not self.rgame.install_path or not path:
|
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)
|
src_path = os.path.realpath(self.rgame.install_path)
|
||||||
dst_path = os.path.realpath(path)
|
dst_path = os.path.realpath(path)
|
||||||
dst_install_path = os.path.realpath(os.path.join(dst_path, os.path.basename(src_path)))
|
dst_install_path = os.path.realpath(os.path.join(dst_path, os.path.basename(src_path)))
|
||||||
|
|
||||||
if not os.path.isdir(dst_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
|
# Get free space on drive and size of game folder
|
||||||
_, _, free_space = shutil.disk_usage(dst_path)
|
_, _, free_space = shutil.disk_usage(dst_path)
|
||||||
|
@ -97,20 +116,20 @@ class MoveGamePopUp(QWidget):
|
||||||
self.avail_space.setText(get_size(free_space))
|
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):
|
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:
|
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):
|
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):
|
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():
|
for rgame in self.rcore.installed_games:
|
||||||
if game.install_path in path:
|
if not rgame.is_non_asset and rgame.install_path in path:
|
||||||
return helper_func("Game installations cannot be nested due to unintended sideeffects.")
|
return helper_func(MovePathEditReasons.NESTED_DIR)
|
||||||
|
|
||||||
is_existing_dir = is_game_dir(src_path, dst_install_path)
|
is_existing_dir = is_game_dir(src_path, dst_install_path)
|
||||||
|
|
||||||
|
@ -123,16 +142,16 @@ class MoveGamePopUp(QWidget):
|
||||||
self.warn_label.setHidden(False)
|
self.warn_label.setHidden(False)
|
||||||
|
|
||||||
if free_space <= source_size and not is_existing_dir:
|
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
|
# Fallback
|
||||||
self.button.setEnabled(True)
|
self.button.setEnabled(True)
|
||||||
return True, path, str()
|
return True, path, IndicatorReasonsCommon.VALID
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def __update_widget(self):
|
def __update_widget(self):
|
||||||
""" React to state updates from RareGame """
|
""" 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)
|
self.setDisabled(True)
|
||||||
return
|
return
|
||||||
# FIXME: Make edit_func lighter instead of blocking signals
|
# FIXME: Make edit_func lighter instead of blocking signals
|
||||||
|
|
|
@ -15,7 +15,7 @@ from rare.shared import RareCore
|
||||||
from rare.shared.workers.wine_resolver import WineResolver
|
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_group import Ui_EGLSyncGroup
|
||||||
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
|
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")
|
logger = getLogger("EGLSync")
|
||||||
|
|
||||||
|
@ -92,9 +92,9 @@ class EGLSyncGroup(QGroupBox):
|
||||||
self.egl_path_edit.setText(path)
|
self.egl_path_edit.setText(path)
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
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(
|
if os.path.exists(os.path.join(path, "system.reg")) and os.path.exists(
|
||||||
os.path.join(path, "dosdevices/c:")
|
os.path.join(path, "dosdevices/c:")
|
||||||
):
|
):
|
||||||
|
@ -108,10 +108,10 @@ class EGLSyncGroup(QGroupBox):
|
||||||
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
|
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
|
||||||
):
|
):
|
||||||
# lower() might or might not be needed in the check
|
# 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):
|
if os.path.exists(path):
|
||||||
return True, path, ""
|
return True, path, IndicatorReasonsCommon.VALID
|
||||||
return False, path, PathEdit.reasons.dir_not_exist
|
return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS
|
||||||
|
|
||||||
def egl_path_edit_save_cb(self, path):
|
def egl_path_edit_save_cb(self, path):
|
||||||
if not path or not os.path.exists(path):
|
if not path or not os.path.exists(path):
|
||||||
|
|
|
@ -17,7 +17,7 @@ from rare.lgndr.glue.arguments import LgndrImportGameArgs
|
||||||
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
|
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
|
||||||
from rare.shared import RareCore
|
from rare.shared import RareCore
|
||||||
from rare.ui.components.tabs.games.integrations.import_group import Ui_ImportGroup
|
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
|
from rare.widgets.elide_label import ElideLabel
|
||||||
|
|
||||||
logger = getLogger("Import")
|
logger = getLogger("Import")
|
||||||
|
@ -212,15 +212,15 @@ class ImportGroup(QGroupBox):
|
||||||
|
|
||||||
self.threadpool = QThreadPool.globalInstance()
|
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(path):
|
||||||
if os.path.exists(os.path.join(path, ".egstore")):
|
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:
|
elif os.path.basename(path) in self.install_dir_list:
|
||||||
return True, path, ""
|
return True, path, IndicatorReasonsCommon.VALID
|
||||||
else:
|
else:
|
||||||
return False, path, PathEdit.reasons.dir_not_exist
|
return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS
|
||||||
return False, path, ""
|
return False, path, IndicatorReasonsCommon.UNDEFINED
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def path_changed(self, path: str):
|
def path_changed(self, path: str):
|
||||||
|
@ -231,13 +231,13 @@ class ImportGroup(QGroupBox):
|
||||||
else:
|
else:
|
||||||
self.app_name_edit.setText("")
|
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:
|
if not text:
|
||||||
return False, text, ""
|
return False, text, IndicatorReasonsCommon.UNDEFINED
|
||||||
if text in self.app_name_list:
|
if text in self.app_name_list:
|
||||||
return True, text, ""
|
return True, text, IndicatorReasonsCommon.VALID
|
||||||
else:
|
else:
|
||||||
return False, text, IndicatorLineEdit.reasons.game_not_installed
|
return False, text, IndicatorReasonsCommon.NOT_INSTALLED
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def app_name_changed(self, app_name: str):
|
def app_name_changed(self, app_name: str):
|
||||||
|
|
|
@ -3,12 +3,12 @@ import re
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Tuple
|
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 PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
|
||||||
|
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
|
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
|
from rare.utils.misc import get_size
|
||||||
|
|
||||||
logger = getLogger("LegendarySettings")
|
logger = getLogger("LegendarySettings")
|
||||||
|
@ -102,18 +102,17 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
||||||
QThreadPool.globalInstance().start(worker)
|
QThreadPool.globalInstance().start(worker)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def locale_edit_cb(text: str) -> Tuple[bool, str, str]:
|
def locale_edit_cb(text: str) -> Tuple[bool, str, int]:
|
||||||
if text:
|
if text:
|
||||||
if re.match("^[a-zA-Z]{2,3}[-_][a-zA-Z]{2,3}$", text):
|
if re.match("^[a-zA-Z]{2,3}[-_][a-zA-Z]{2,3}$", text):
|
||||||
language, country = text.replace("_", "-").split("-")
|
language, country = text.replace("_", "-").split("-")
|
||||||
text = "-".join([language.lower(), country.upper()])
|
text = "-".join([language.lower(), country.upper()])
|
||||||
if bool(re.match("^[a-z]{2,3}-[A-Z]{2,3}$", text)):
|
if bool(re.match("^[a-z]{2,3}-[A-Z]{2,3}$", text)):
|
||||||
return True, text, ""
|
return True, text, IndicatorReasonsCommon.VALID
|
||||||
else:
|
else:
|
||||||
return False, text, IndicatorLineEdit.reasons.wrong_format
|
return False, text, IndicatorReasonsCommon.WRONG_FORMAT
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return True, text, ""
|
return True, text, IndicatorReasonsCommon.VALID
|
||||||
|
|
||||||
def locale_save_cb(self, text: str):
|
def locale_save_cb(self, text: str):
|
||||||
if text:
|
if text:
|
||||||
|
|
|
@ -9,7 +9,7 @@ from rare.components.tabs.settings.widgets.mangohud import MangoHudSettings
|
||||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||||
|
|
||||||
from rare.ui.components.tabs.settings.linux import Ui_LinuxSettings
|
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")
|
logger = getLogger("LinuxSettings")
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ class LinuxSettings(QWidget, Ui_LinuxSettings):
|
||||||
self.wine_prefix = PathEdit(
|
self.wine_prefix = PathEdit(
|
||||||
self.load_prefix(),
|
self.load_prefix(),
|
||||||
file_type=QFileDialog.DirectoryOnly,
|
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,
|
save_func=self.save_prefix,
|
||||||
)
|
)
|
||||||
self.prefix_layout.addWidget(self.wine_prefix)
|
self.prefix_layout.addWidget(self.wine_prefix)
|
||||||
|
@ -38,7 +38,7 @@ class LinuxSettings(QWidget, Ui_LinuxSettings):
|
||||||
self.load_setting(self.name, "wine_executable"),
|
self.load_setting(self.name, "wine_executable"),
|
||||||
file_type=QFileDialog.ExistingFile,
|
file_type=QFileDialog.ExistingFile,
|
||||||
name_filter="Wine executable (wine wine64)",
|
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(
|
save_func=lambda text: self.save_setting(
|
||||||
text, section=self.name, setting="wine_executable"
|
text, section=self.name, setting="wine_executable"
|
||||||
),
|
),
|
||||||
|
|
|
@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog
|
||||||
|
|
||||||
from rare.shared import LegendaryCoreSingleton
|
from rare.shared import LegendaryCoreSingleton
|
||||||
from rare.utils import config_helper
|
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):
|
class PreLaunchSettings(QHBoxLayout):
|
||||||
|
@ -28,14 +28,14 @@ class PreLaunchSettings(QHBoxLayout):
|
||||||
self.layout().addWidget(self.wait_check)
|
self.layout().addWidget(self.wait_check)
|
||||||
self.wait_check.stateChanged.connect(self.save_wait_finish)
|
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():
|
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]):
|
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:
|
else:
|
||||||
return True, text, ""
|
return True, text, IndicatorReasonsCommon.VALID
|
||||||
|
|
||||||
def save_pre_launch_command(self, text):
|
def save_pre_launch_command(self, text):
|
||||||
if text:
|
if text:
|
||||||
|
|
|
@ -9,7 +9,7 @@ from rare.components.tabs.settings import LinuxSettings
|
||||||
from .wrapper import WrapperSettings
|
from .wrapper import WrapperSettings
|
||||||
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings
|
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings
|
||||||
from rare.utils import config_helper
|
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
|
from rare.shared import LegendaryCoreSingleton
|
||||||
|
|
||||||
logger = getLogger("Proton")
|
logger = getLogger("Proton")
|
||||||
|
@ -103,12 +103,12 @@ class ProtonSettings(QGroupBox, Ui_ProtonSettings):
|
||||||
|
|
||||||
config_helper.save_config()
|
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:
|
if not text:
|
||||||
text = os.path.expanduser("~/.proton")
|
text = os.path.expanduser("~/.proton")
|
||||||
return True, text, ""
|
return True, text, IndicatorReasonsCommon.VALID
|
||||||
parent_dir = os.path.dirname(text)
|
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):
|
def proton_prefix_save(self, text: str):
|
||||||
if not self.changeable:
|
if not self.changeable:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
from enum import IntEnum
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Callable, Tuple, Optional
|
from typing import Callable, Tuple, Optional, Dict
|
||||||
|
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
Qt,
|
Qt,
|
||||||
|
@ -10,6 +11,10 @@ from PyQt5.QtCore import (
|
||||||
QPoint,
|
QPoint,
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QFileInfo,
|
QFileInfo,
|
||||||
|
QRunnable,
|
||||||
|
QObject,
|
||||||
|
QThreadPool,
|
||||||
|
pyqtSlot,
|
||||||
)
|
)
|
||||||
from PyQt5.QtGui import QMovie, QPixmap, QFontMetrics, QImage
|
from PyQt5.QtGui import QMovie, QPixmap, QFontMetrics, QImage
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
|
@ -34,34 +39,94 @@ from PyQt5.QtWidgets import (
|
||||||
QScrollArea,
|
QScrollArea,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from rare.utils.misc import icon as qta_icon
|
||||||
from rare.utils.paths import tmp_dir
|
from rare.utils.paths import tmp_dir
|
||||||
from rare.utils.qt_requests import QtRequestManager
|
from rare.utils.qt_requests import QtRequestManager
|
||||||
from rare.utils.misc import icon as qta_icon
|
|
||||||
|
|
||||||
logger = getLogger("ExtraWidgets")
|
logger = getLogger("ExtraWidgets")
|
||||||
|
|
||||||
|
|
||||||
class IndicatorReasons:
|
class IndicatorReasons(IntEnum):
|
||||||
dir_not_empty = QCoreApplication.translate("IndicatorReasons", "Directory is not empty")
|
"""
|
||||||
wrong_format = QCoreApplication.translate("IndicatorReasons", "Given text has wrong format")
|
Empty enumeration with auto-generated enumeration values.
|
||||||
game_not_installed = QCoreApplication.translate(
|
Extend this class per-case to implement dedicated message types.
|
||||||
"IndicatorReasons", "Game is not installed or does not exist"
|
Types should be assigned using `auto()` from enum
|
||||||
)
|
|
||||||
dir_not_exist = QCoreApplication.translate("IndicatorReasons", "Directory does not exist")
|
example:
|
||||||
file_not_exist = QCoreApplication.translate("IndicatorReasons", "File does not exist")
|
MyReasons(IndicatorReasons):
|
||||||
wrong_path = QCoreApplication.translate("IndicatorReasons", "Wrong Directory")
|
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):
|
class IndicatorLineEdit(QWidget):
|
||||||
textChanged = pyqtSignal(str)
|
textChanged = pyqtSignal(str)
|
||||||
reasons = IndicatorReasons()
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
text: str = "",
|
text: str = "",
|
||||||
placeholder: str = "",
|
placeholder: str = "",
|
||||||
completer: QCompleter = None,
|
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,
|
save_func: Callable[[str], None] = None,
|
||||||
horiz_policy: QSizePolicy.Policy = QSizePolicy.Expanding,
|
horiz_policy: QSizePolicy.Policy = QSizePolicy.Expanding,
|
||||||
parent=None,
|
parent=None,
|
||||||
|
@ -99,6 +164,12 @@ class IndicatorLineEdit(QWidget):
|
||||||
_translate = QCoreApplication.instance().translate
|
_translate = QCoreApplication.instance().translate
|
||||||
self.line_edit.setPlaceholderText(_translate(self.__class__.__name__, "Default"))
|
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.is_valid = False
|
||||||
self.edit_func = edit_func
|
self.edit_func = edit_func
|
||||||
self.save_func = save_func
|
self.save_func = save_func
|
||||||
|
@ -125,27 +196,38 @@ class IndicatorLineEdit(QWidget):
|
||||||
self.hint_label.setFrameRect(self.line_edit.rect())
|
self.hint_label.setFrameRect(self.line_edit.rect())
|
||||||
self.hint_label.setText(text)
|
self.hint_label.setText(text)
|
||||||
|
|
||||||
def __indicator(self, res, reason=None):
|
def extend_reasons(self, reasons: Dict):
|
||||||
color = "green" if res else "red"
|
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))
|
self.indicator_label.setPixmap(qta_icon("ei.info-circle", color=color).pixmap(16, 16))
|
||||||
if reason:
|
if not valid:
|
||||||
self.indicator_label.setToolTip(reason)
|
self.indicator_label.setToolTip(self.__reasons[reason])
|
||||||
else:
|
else:
|
||||||
self.indicator_label.setToolTip("")
|
self.indicator_label.setToolTip(self.__reasons[IndicatorReasonsCommon.VALID])
|
||||||
|
|
||||||
def __edit(self, text):
|
@pyqtSlot(bool, str, int)
|
||||||
if self.edit_func is not None:
|
def __edit_handler(self, is_valid: bool, text: str, reason: int):
|
||||||
|
self.__thread = None
|
||||||
self.line_edit.blockSignals(True)
|
self.line_edit.blockSignals(True)
|
||||||
|
|
||||||
self.is_valid, text, reason = self.edit_func(text)
|
|
||||||
if text != self.line_edit.text():
|
if text != self.line_edit.text():
|
||||||
self.line_edit.setText(text)
|
self.line_edit.setText(text)
|
||||||
self.line_edit.blockSignals(False)
|
self.line_edit.blockSignals(False)
|
||||||
self.__indicator(self.is_valid, reason)
|
self.__indicator(is_valid, reason)
|
||||||
if self.is_valid:
|
if is_valid:
|
||||||
self.__save(text)
|
self.__save(text)
|
||||||
|
self.is_valid = is_valid
|
||||||
self.textChanged.emit(text)
|
self.textChanged.emit(text)
|
||||||
|
|
||||||
|
def __edit(self, text):
|
||||||
|
if self.edit_func is not None:
|
||||||
|
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):
|
def __save(self, text):
|
||||||
if self.save_func is not None:
|
if self.save_func is not None:
|
||||||
self.save_func(text)
|
self.save_func(text)
|
||||||
|
@ -166,8 +248,8 @@ class PathEditIconProvider(QFileIconProvider):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(PathEditIconProvider, self).__init__()
|
super(PathEditIconProvider, self).__init__()
|
||||||
self.icon_types = dict()
|
self.icon_types = {}
|
||||||
for idx, (icn, fallback) in enumerate(self.icons):
|
for idx, (icn, fallback) in enumerate(PathEditIconProvider.icons):
|
||||||
self.icon_types.update({idx - 1: qta_icon(icn, fallback, color="#eeeeee")})
|
self.icon_types.update({idx - 1: qta_icon(icn, fallback, color="#eeeeee")})
|
||||||
|
|
||||||
def icon(self, info_type):
|
def icon(self, info_type):
|
||||||
|
@ -192,7 +274,7 @@ class PathEdit(IndicatorLineEdit):
|
||||||
type_filter: str = "",
|
type_filter: str = "",
|
||||||
name_filter: str = "",
|
name_filter: str = "",
|
||||||
placeholder: 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,
|
save_func: Callable[[str], None] = None,
|
||||||
horiz_policy: QSizePolicy.Policy = QSizePolicy.Expanding,
|
horiz_policy: QSizePolicy.Policy = QSizePolicy.Expanding,
|
||||||
parent=None,
|
parent=None,
|
||||||
|
@ -230,8 +312,7 @@ class PathEdit(IndicatorLineEdit):
|
||||||
layout = self.layout()
|
layout = self.layout()
|
||||||
layout.addWidget(self.path_select)
|
layout.addWidget(self.path_select)
|
||||||
|
|
||||||
_translate = QCoreApplication.instance().translate
|
self.path_select.setText(self.tr("Browse..."))
|
||||||
self.path_select.setText(_translate("PathEdit", "Browse..."))
|
|
||||||
|
|
||||||
self.type_filter = type_filter
|
self.type_filter = type_filter
|
||||||
self.name_filter = name_filter
|
self.name_filter = name_filter
|
||||||
|
@ -254,11 +335,12 @@ class PathEdit(IndicatorLineEdit):
|
||||||
self.line_edit.setText(names[0])
|
self.line_edit.setText(names[0])
|
||||||
self.compl_model.setRootPath(names[0])
|
self.compl_model.setRootPath(names[0])
|
||||||
|
|
||||||
def __wrap_edit_function(self, edit_function: Callable[[str], Tuple[bool, str, str]]):
|
@staticmethod
|
||||||
if edit_function:
|
def __wrap_edit_function(func: Callable[[str], Tuple[bool, str, int]]):
|
||||||
return lambda text: edit_function(os.path.expanduser(text) if text.startswith("~") else text)
|
if func:
|
||||||
|
return lambda text: func(os.path.expanduser(text) if text.startswith("~") else text)
|
||||||
else:
|
else:
|
||||||
return edit_function
|
return func
|
||||||
|
|
||||||
|
|
||||||
class SideTabBar(QTabBar):
|
class SideTabBar(QTabBar):
|
||||||
|
|
|
@ -144,9 +144,9 @@ def get_latest_version():
|
||||||
|
|
||||||
|
|
||||||
def get_size(b: Union[int, float]) -> str:
|
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:
|
if b < 1024:
|
||||||
return f"{b:.2f} {s}iB"
|
return f"{b:.2f} {s}B"
|
||||||
b /= 1024
|
b /= 1024
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue