diff --git a/rare/commands/launcher/__init__.py b/rare/commands/launcher/__init__.py index 8e06ea0bde..636312efbc 100644 --- a/rare/commands/launcher/__init__.py +++ b/rare/commands/launcher/__init__.py @@ -29,7 +29,7 @@ from PySide6.QtWidgets import QApplication from rare.lgndr.core import LegendaryCore -from rare.models.base_game import RareGameSlim +from rare.models.game_slim import RareGameSlim from rare.models.launcher import Actions, BaseModel, ErrorModel, FinishedModel, StateChangedModel from rare.shared.workers.cloud_sync import CloudSyncWorker from rare.utils.paths import get_rare_executable @@ -50,16 +50,18 @@ } +class PreLaunchSignals(QObject): + ready_to_launch = Signal(LaunchParams) + pre_launch_command_started = Signal() + pre_launch_command_finished = Signal(int) # exit_code + error_occurred = Signal(str) + + class PreLaunch(QRunnable): - class Signals(QObject): - ready_to_launch = Signal(LaunchParams) - pre_launch_command_started = Signal() - pre_launch_command_finished = Signal(int) # exit_code - error_occurred = Signal(str) def __init__(self, args: InitParams, rgame: RareGameSlim, sync_action=None): super(PreLaunch, self).__init__() - self.signals = self.Signals() + self.signals = PreLaunchSignals() self.logger = getLogger(type(self).__name__) self.args = args self.rgame = rgame @@ -76,8 +78,8 @@ def run(self) -> None: if args := self.prepare_launch(self.args): self.signals.ready_to_launch.emit(args) - else: - return + self.signals.disconnect(self.signals) + self.signals.deleteLater() def prepare_launch(self, args: InitParams) -> Optional[LaunchParams]: try: @@ -92,7 +94,8 @@ def prepare_launch(self, args: InitParams) -> Optional[LaunchParams]: proc = get_configured_qprocess(shlex.split(launch.pre_launch_command), launch.environment) proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) proc.readyReadStandardOutput.connect( - lambda: self.logger.info(str(proc.readAllStandardOutput().data(), "utf-8", "ignore")) + (lambda obj: obj.logger.debug( + str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))).__get__(self) ) self.signals.pre_launch_command_started.emit() self.logger.info("Running pre-launch command %s, %s", proc.program(), proc.arguments()) @@ -102,17 +105,20 @@ def prepare_launch(self, args: InitParams) -> Optional[LaunchParams]: proc.waitForFinished(-1) else: proc.startDetached() + return launch +class SyncCheckWorkerSignals(QObject): + sync_state_ready = Signal() + error_occurred = Signal(str) + + class SyncCheckWorker(QRunnable): - class Signals(QObject): - sync_state_ready = Signal() - error_occurred = Signal(str) def __init__(self, core: LegendaryCore, rgame: RareGameSlim): super().__init__() - self.signals = self.Signals() + self.signals = SyncCheckWorkerSignals() self.core = core self.rgame = rgame @@ -123,6 +129,8 @@ def run(self) -> None: self.signals.error_occurred.emit(str(e)) return self.signals.sync_state_ready.emit() + self.signals.disconnect(self.signals) + self.signals.deleteLater() class RareLauncherException(RareAppException): @@ -171,26 +179,17 @@ def __init__(self, args: InitParams): if args.show_console: self.console = ConsoleDialog(game.app_title) self.console.show() - self.game_process.stateChanged.connect( - lambda s: self.console.kill_button.setEnabled( - self.game_process.state() == QProcess.ProcessState.Running - ) - ) - self.game_process.stateChanged.connect( - lambda s: self.console.terminate_button.setEnabled( - self.game_process.state() == QProcess.ProcessState.Running - ) - ) + self.game_process.stateChanged.connect(self._on_game_process_changed) self.sync_dialog: Optional[CloudSyncDialog] = None self.game_process.finished.connect(self.__process_finished) self.game_process.errorOccurred.connect(self.__process_errored) if self.console: - self.game_process.readyReadStandardOutput.connect(self.__proc_log_stdout) - self.game_process.readyReadStandardError.connect(self.__proc_log_stderr) - self.console.term.connect(self.__proc_term) - self.console.kill.connect(self.__proc_kill) + self.game_process.readyReadStandardOutput.connect(self._proc_log_stdout) + self.game_process.readyReadStandardError.connect(self._proc_log_stderr) + self.console.term.connect(self._proc_term) + self.console.kill.connect(self._proc_kill) ret = self.server.listen(f"rare_{args.app_name}") if not ret: @@ -207,23 +206,28 @@ def __init__(self, args: InitParams): # The timer's signal will be serviced once we call `exec()` on the application QTimer.singleShot(0, self.start) + @Slot(QProcess.ProcessState) + def _on_game_process_changed(self, state: QProcess.ProcessState): + self.console.kill_button.setEnabled(state == QProcess.ProcessState.Running) + self.console.terminate_button.setEnabled(state == QProcess.ProcessState.Running) + @Slot() - def __proc_log_stdout(self): + def _proc_log_stdout(self): self.console.log_stdout(self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")) @Slot() - def __proc_log_stderr(self): + def _proc_log_stderr(self): self.console.log_stderr(self.game_process.readAllStandardError().data().decode("utf-8", "ignore")) @Slot() - def __proc_term(self): + def _proc_term(self): if platform.system() == "Windows": self.game_process.terminate() else: os.kill(self.game_process.processId(), signal.SIGINT) @Slot() - def __proc_kill(self): + def _proc_kill(self): if platform.system() == "Windows": self.game_process.kill() else: @@ -251,23 +255,32 @@ def send_message(self, message: BaseModel): else: self.logger.error("Can't send message") - def check_saves_finished(self, exit_code: int): - self.rgame.signals.widget.update.connect(lambda: self.on_exit(exit_code)) + def check_saves(self, exit_code: int): + # self.rgame.signals.widget.refresh.connect(lambda: self.on_exit(exit_code)) + self.rgame.signals.widget.refresh.connect( + (lambda obj: obj.on_exit(exit_code)).__get__(self) + ) state, (dt_local, dt_remote) = self.rgame.save_game_state if state == SaveGameStatus.LOCAL_NEWER and not self.no_sync_on_exit: action = CloudSyncDialogResult.UPLOAD - self.__check_saves_finished(exit_code, action) + self.check_saves_finished(exit_code, action) else: self.sync_dialog = CloudSyncDialog(self.rgame.igame, dt_local, dt_remote) - self.sync_dialog.result_ready.connect(lambda a: self.__check_saves_finished(exit_code, a)) + # self.sync_dialog.result_ready.connect( + # lambda a: self.__check_saves_finished(exit_code, a) + # ) + self.sync_dialog.result_ready.connect( + (lambda obj, a: obj.check_saves_finished(exit_code, a)).__get__(self) + ) self.sync_dialog.open() @Slot(int, int) @Slot(int, CloudSyncDialogResult) - def __check_saves_finished(self, exit_code, action): + def check_saves_finished(self, exit_code, action): if self.sync_dialog is not None: + self.sync_dialog.disconnect(self.sync_dialog) self.sync_dialog.deleteLater() self.sync_dialog = None action = CloudSyncDialogResult(action) @@ -291,7 +304,7 @@ def __process_finished(self, exit_code: int, exit_status: QProcess.ExitStatus): self.logger.info("Game finished") if self.rgame.auto_sync_saves: - self.check_saves_finished(exit_code) + self.check_saves(exit_code) else: self.on_exit(exit_code) @@ -482,9 +495,9 @@ def stop(self, sig: int = signal.SIGINT): if shiboken6.isValid(self.game_process): # pylint: disable=E1101 if self.game_process.state() != QProcess.ProcessState.NotRunning: if sig == signal.SIGTERM: - self.__proc_term() + self._proc_term() elif sig == signal.SIGINT: - self.__proc_kill() + self._proc_kill() self.game_process.waitForFinished() exit_code = self.game_process.exitCode() self.game_process.deleteLater() diff --git a/rare/commands/launcher/console_dialog.py b/rare/commands/launcher/console_dialog.py index 95fb12e360..587ff74056 100644 --- a/rare/commands/launcher/console_dialog.py +++ b/rare/commands/launcher/console_dialog.py @@ -55,13 +55,13 @@ def __init__(self, app_title: str, parent=None): self.terminate_button = QPushButton(self.tr("Terminate")) # self.terminate_button.setVisible(platform.system() == "Windows") button_layout.addWidget(self.terminate_button) - self.terminate_button.clicked.connect(lambda: self.term.emit()) + self.terminate_button.clicked.connect(self.term) self.terminate_button.setEnabled(False) self.kill_button = QPushButton(self.tr("Kill")) # self.kill_button.setVisible(platform.system() == "Windows") button_layout.addWidget(self.kill_button) - self.kill_button.clicked.connect(lambda: self.kill.emit()) + self.kill_button.clicked.connect(self.kill) self.kill_button.setEnabled(False) layout.addLayout(button_layout) diff --git a/rare/commands/launcher/lgd_helper.py b/rare/commands/launcher/lgd_helper.py index ce9cadf566..48ee17888e 100644 --- a/rare/commands/launcher/lgd_helper.py +++ b/rare/commands/launcher/lgd_helper.py @@ -10,7 +10,7 @@ from legendary.models.game import LaunchParameters from PySide6.QtCore import QProcess, QProcessEnvironment -from rare.models.base_game import RareGameSlim +from rare.models.game_slim import RareGameSlim from rare.utils.paths import setup_compat_shaders_dir logger = getLogger("RareLauncherUtils") diff --git a/rare/components/dialogs/install/dialog.py b/rare/components/dialogs/install/dialog.py index d7f952e3c1..592b5c1684 100644 --- a/rare/components/dialogs/install/dialog.py +++ b/rare/components/dialogs/install/dialog.py @@ -57,7 +57,7 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.__queue_item: Optional[InstallQueueItemModel] = None self.selectable = InstallDialogSelective(rgame, parent=self) - self.selectable.stateChanged.connect(self.__on_option_changed) + self.selectable.stateChanged.connect(self._on_option_changed) self.ui.main_layout.insertRow( self.ui.main_layout.getWidgetPosition(self.ui.shortcut_label)[0] + 1, # self.tr("Optional"), @@ -102,13 +102,13 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.ui.shortcut_label.setDisabled(rgame.is_installed or rgame.is_dlc) self.ui.shortcut_check.setDisabled(rgame.is_installed or rgame.is_dlc) self.ui.shortcut_check.setChecked(not rgame.is_installed and self.settings.get_value(app_settings.create_shortcut)) - self.ui.shortcut_check.checkStateChanged.connect(self.__on_option_changed_no_reload) + self.ui.shortcut_check.checkStateChanged.connect(self._on_option_changed_no_reload) self.set_error_labels() self.ui.platform_combo.addItems(reversed(rgame.platforms)) self.ui.platform_combo.setCurrentIndex(self.ui.platform_combo.findText(options.platform)) - self.ui.platform_combo.currentIndexChanged.connect(self.__on_option_changed) + self.ui.platform_combo.currentIndexChanged.connect(self._on_option_changed) self.ui.platform_combo.currentIndexChanged.connect(self.check_incompatible_platform) self.ui.platform_combo.currentIndexChanged.connect(self.reset_install_dir) self.ui.platform_combo.currentTextChanged.connect(self.selectable.update_list) @@ -122,17 +122,28 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.selectable.click() self.advanced.ui.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0)) - self.advanced.ui.max_workers_spin.valueChanged.connect(self.__on_option_changed) + self.advanced.ui.max_workers_spin.valueChanged.connect(self._on_option_changed) self.advanced.ui.max_memory_spin.setValue(self.core.lgd.config.getint("Legendary", "max_memory", fallback=0)) - self.advanced.ui.max_memory_spin.valueChanged.connect(self.__on_option_changed) + self.advanced.ui.max_memory_spin.valueChanged.connect(self._on_option_changed) - self.advanced.ui.read_files_check.checkStateChanged.connect(self.__on_option_changed) - self.advanced.ui.use_signed_urls_check.checkStateChanged.connect(self.__on_option_changed) - self.advanced.ui.dl_optimizations_check.checkStateChanged.connect(self.__on_option_changed) - self.advanced.ui.force_download_check.checkStateChanged.connect(self.__on_option_changed) - self.advanced.ui.ignore_space_check.checkStateChanged.connect(self.__on_option_changed) - self.advanced.ui.download_only_check.checkStateChanged.connect(self.__on_option_changed_no_reload) + self.advanced.ui.read_files_check.setChecked(options.read_files) + self.advanced.ui.read_files_check.checkStateChanged.connect(self._on_option_changed) + + self.advanced.ui.use_signed_urls_check.setChecked(options.always_use_signed_urls) + self.advanced.ui.use_signed_urls_check.checkStateChanged.connect(self._on_option_changed) + + self.advanced.ui.dl_optimizations_check.setChecked(options.order_opt) + self.advanced.ui.dl_optimizations_check.checkStateChanged.connect(self._on_option_changed) + + self.advanced.ui.force_download_check.setChecked(options.force) + self.advanced.ui.force_download_check.checkStateChanged.connect(self._on_option_changed) + + self.advanced.ui.ignore_space_check.setChecked(options.ignore_space) + self.advanced.ui.ignore_space_check.checkStateChanged.connect(self._on_option_changed) + + self.advanced.ui.download_only_check.setChecked(options.no_install) + self.advanced.ui.download_only_check.checkStateChanged.connect(self._on_option_changed_no_reload) self.reset_install_dir(self.ui.platform_combo.currentIndex()) self.selectable.update_list(self.ui.platform_combo.currentText()) @@ -159,7 +170,7 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.advanced.ui.install_prereqs_label.setEnabled(False) self.advanced.ui.install_prereqs_check.setEnabled(False) - self.advanced.ui.install_prereqs_check.checkStateChanged.connect(self.__on_option_changed_no_reload) + self.advanced.ui.install_prereqs_check.checkStateChanged.connect(self._on_option_changed_no_reload) self.advanced.ui.install_prereqs_check.setChecked(self.__options.install_prereqs) # lk: set object names for CSS properties @@ -241,13 +252,13 @@ def action_handler(self): self.get_download_info() @Slot() - def __on_option_changed(self): + def _on_option_changed(self): self.options_changed = True self.accept_button.setEnabled(False) self.action_button.setEnabled(not self.active()) @Slot(Qt.CheckState) - def __on_option_changed_no_reload(self, state: Qt.CheckState): + def _on_option_changed_no_reload(self, state: Qt.CheckState): if self.sender() is self.advanced.ui.download_only_check: self.__options.no_install = state != Qt.CheckState.Unchecked elif self.sender() is self.ui.shortcut_check: diff --git a/rare/components/dialogs/install/selective.py b/rare/components/dialogs/install/selective.py index 8a8f5ff365..cf79b4e4f0 100644 --- a/rare/components/dialogs/install/selective.py +++ b/rare/components/dialogs/install/selective.py @@ -75,6 +75,7 @@ def __init__(self, rgame: RareGame, parent=None): def update_list(self, platform: str): if self.widget is not None: + self.widget.disconnect(self.widget) self.widget.deleteLater() self.widget = SelectiveWidget(self.rgame, platform, parent=self) self.widget.stateChanged.connect(self.stateChanged) diff --git a/rare/components/dialogs/login/__init__.py b/rare/components/dialogs/login/__init__.py index c5db7c3ef5..cd52bd6edf 100644 --- a/rare/components/dialogs/login/__init__.py +++ b/rare/components/dialogs/login/__init__.py @@ -52,12 +52,12 @@ def __init__(self, args: Namespace, core: LegendaryCore, parent=None): self.browser_page = BrowserLogin(self.core, self.login_stack) self.browser_index = self.login_stack.insertWidget(1, self.browser_page) - self.browser_page.success.connect(self.login_successful) - self.browser_page.isValid.connect(lambda x: self.ui.next_button.setEnabled(x)) + self.browser_page.success.connect(self._on_login_successful) + self.browser_page.validated.connect(self._on_page_validated) self.import_page = ImportLogin(self.core, self.login_stack) self.import_index = self.login_stack.insertWidget(2, self.import_page) - self.import_page.success.connect(self.login_successful) - self.import_page.isValid.connect(lambda x: self.ui.next_button.setEnabled(x)) + self.import_page.success.connect(self._on_login_successful) + self.import_page.validated.connect(self._on_page_validated) self.info_message = { self.landing_index: self.tr( @@ -87,14 +87,14 @@ def __init__(self, args: Namespace, core: LegendaryCore, parent=None): self.ui.next_button.setEnabled(False) self.ui.back_button.setEnabled(False) - self.landing_page.ui.login_browser_radio.clicked.connect(lambda: self.ui.next_button.setEnabled(True)) - self.landing_page.ui.login_browser_radio.clicked.connect(self.browser_radio_clicked) - self.landing_page.ui.login_import_radio.clicked.connect(lambda: self.ui.next_button.setEnabled(True)) - self.landing_page.ui.login_import_radio.clicked.connect(self.import_radio_clicked) + self.landing_page.ui.login_browser_radio.clicked.connect(self._on_radio_clicked) + self.landing_page.ui.login_browser_radio.clicked.connect(self._on_browser_radio_clicked) + self.landing_page.ui.login_import_radio.clicked.connect(self._on_radio_clicked) + self.landing_page.ui.login_import_radio.clicked.connect(self._on_import_radio_clicked) self.ui.exit_button.clicked.connect(self.reject) - self.ui.back_button.clicked.connect(self.back_clicked) - self.ui.next_button.clicked.connect(self.next_clicked) + self.ui.back_button.clicked.connect(self._on_back_clicked) + self.ui.next_button.clicked.connect(self._on_next_clicked) self.login_stack.setCurrentWidget(self.landing_page) @@ -110,33 +110,41 @@ def __init__(self, args: Namespace, core: LegendaryCore, parent=None): self.ui.main_layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize) @Slot() - def browser_radio_clicked(self): + def _on_radio_clicked(self): + self.ui.next_button.setEnabled(True) + + @Slot(bool) + def _on_page_validated(self, valid: bool): + self.ui.next_button.setEnabled(valid) + + @Slot() + def _on_browser_radio_clicked(self): self.login_stack.slideInWidget(self.browser_page) self.ui.info_label.setText(self.info_message[self.browser_index]) self.ui.back_button.setEnabled(True) self.ui.next_button.setEnabled(False) @Slot() - def import_radio_clicked(self): + def _on_import_radio_clicked(self): self.login_stack.slideInWidget(self.import_page) self.ui.info_label.setText(self.info_message[self.import_index]) self.ui.back_button.setEnabled(True) self.ui.next_button.setEnabled(self.import_page.is_valid()) @Slot() - def back_clicked(self): + def _on_back_clicked(self): self.ui.back_button.setEnabled(False) self.ui.next_button.setEnabled(True) self.ui.info_label.setText(self.info_message[self.landing_index]) self.login_stack.slideInWidget(self.landing_page) @Slot() - def next_clicked(self): + def _on_next_clicked(self): if self.login_stack.currentWidget() is self.landing_page: if self.landing_page.ui.login_browser_radio.isChecked(): - self.browser_radio_clicked() + self._on_browser_radio_clicked() if self.landing_page.ui.login_import_radio.isChecked(): - self.import_radio_clicked() + self._on_import_radio_clicked() elif self.login_stack.currentWidget() is self.browser_page: self.browser_page.do_login() elif self.login_stack.currentWidget() is self.import_page: @@ -148,7 +156,7 @@ def login(self): self.open() @Slot() - def login_successful(self): + def _on_login_successful(self): try: if not self.core.login(): raise ValueError("Login failed.") diff --git a/rare/components/dialogs/login/browser_login.py b/rare/components/dialogs/login/browser_login.py index 568d3f658c..b2bdc0cff0 100644 --- a/rare/components/dialogs/login/browser_login.py +++ b/rare/components/dialogs/login/browser_login.py @@ -1,11 +1,10 @@ import json -from logging import getLogger from typing import Tuple from legendary.utils import webview_login -from PySide6.QtCore import QProcess, QUrl, Signal, Slot +from PySide6.QtCore import QProcess, QUrl, Slot from PySide6.QtGui import QDesktopServices -from PySide6.QtWidgets import QApplication, QFormLayout, QFrame, QLineEdit +from PySide6.QtWidgets import QApplication, QFormLayout, QLineEdit from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.exception import LgndrException @@ -14,20 +13,17 @@ from rare.utils.paths import get_rare_executable from rare.widgets.indicator_edit import IndicatorLineEdit, IndicatorReasonsCommon +from .login_frame import LoginFrame -class BrowserLogin(QFrame): - success = Signal() - isValid = Signal(bool) + +class BrowserLogin(LoginFrame): def __init__(self, core: LegendaryCore, parent=None): - super(BrowserLogin, self).__init__(parent=parent) - self.logger = getLogger(type(self).__name__) + super(BrowserLogin, self).__init__(core, parent=parent) - self.setFrameStyle(QFrame.Shape.StyledPanel) self.ui = Ui_BrowserLogin() self.ui.setupUi(self) - self.core = core self.login_url = self.core.egs.get_auth_url() self.auth_edit = IndicatorLineEdit( @@ -36,23 +32,23 @@ def __init__(self, core: LegendaryCore, parent=None): self.auth_edit.line_edit.setEchoMode(QLineEdit.EchoMode.Password) self.ui.link_text.setText(self.login_url) self.ui.copy_button.setIcon(qta_icon("mdi.content-copy", "fa5.copy")) - self.ui.copy_button.clicked.connect(self.copy_link) + self.ui.copy_button.clicked.connect(self._on_copy_link) self.ui.form_layout.setWidget( self.ui.form_layout.getWidgetPosition(self.ui.sid_label)[0], QFormLayout.ItemRole.FieldRole, self.auth_edit ) - self.ui.open_button.clicked.connect(self.open_browser) - self.auth_edit.textChanged.connect(lambda _: self.isValid.emit(self.is_valid())) + self.ui.open_button.clicked.connect(self._on_open_browser) + self.auth_edit.textChanged.connect(self._on_input_changed) @Slot() - def copy_link(self): + def _on_copy_link(self): clipboard = QApplication.instance().clipboard() clipboard.setText(self.login_url) self.ui.status_field.setText(self.tr("Copied to clipboard")) - def is_valid(self): + def is_valid(self) -> bool: return self.auth_edit.is_valid @staticmethod @@ -70,7 +66,7 @@ def sid_edit_callback(text) -> Tuple[bool, str, int]: else: return False, text, IndicatorReasonsCommon.VALID - def do_login(self): + def do_login(self) -> None: self.ui.status_field.setText(self.tr("Logging in...")) auth_code = self.auth_edit.text() try: @@ -84,7 +80,7 @@ def do_login(self): self.logger.error(e) @Slot() - def open_browser(self): + def _on_open_browser(self): if not webview_login.webview_available: self.logger.warning("You don't have webengine installed, you will need to manually copy the authorizationCode.") QDesktopServices.openUrl(QUrl(self.login_url)) diff --git a/rare/components/dialogs/login/import_login.py b/rare/components/dialogs/login/import_login.py index 00294841ba..eee72eba7e 100644 --- a/rare/components/dialogs/login/import_login.py +++ b/rare/components/dialogs/login/import_login.py @@ -1,20 +1,19 @@ import os import platform from getpass import getuser -from logging import getLogger from legendary.lfs.wine_helpers import get_shell_folders, read_registry -from PySide6.QtCore import Signal, Slot -from PySide6.QtWidgets import QFileDialog, QFrame +from PySide6.QtCore import Slot +from PySide6.QtWidgets import QFileDialog from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.exception import LgndrException from rare.ui.components.dialogs.login.import_login import Ui_ImportLogin +from .login_frame import LoginFrame -class ImportLogin(QFrame): - success = Signal() - isValid = Signal(bool) + +class ImportLogin(LoginFrame): # FIXME: Use pathspec instead of duplicated code if platform.system() == "Windows": @@ -25,15 +24,11 @@ class ImportLogin(QFrame): found = False def __init__(self, core: LegendaryCore, parent=None): - super(ImportLogin, self).__init__(parent=parent) - self.logger = getLogger(type(self).__name__) + super(ImportLogin, self).__init__(core, parent=parent) - self.setFrameStyle(QFrame.Shape.StyledPanel) self.ui = Ui_ImportLogin() self.ui.setupUi(self) - self.core = core - self.text_egl_found = self.tr("Found EGL Program Data. Click 'Next' to import them.") self.text_egl_notfound = self.tr("Could not find EGL Program Data. ") @@ -57,8 +52,8 @@ def __init__(self, core: LegendaryCore, parent=None): else: self.ui.status_field.setText(self.text_egl_notfound) - self.ui.prefix_button.clicked.connect(self.prefix_path) - self.ui.prefix_combo.editTextChanged.connect(lambda _: self.isValid.emit(self.is_valid())) + self.ui.prefix_button.clicked.connect(self._on_prefix_path) + self.ui.prefix_combo.editTextChanged.connect(self._on_input_changed) def get_wine_prefixes(self): possible_prefixes = [ @@ -72,7 +67,7 @@ def get_wine_prefixes(self): return prefixes @Slot() - def prefix_path(self): + def _on_prefix_path(self): prefix_dialog = QFileDialog(self, self.tr("Choose path"), os.path.expanduser("~/")) prefix_dialog.setFileMode(QFileDialog.FileMode.Directory) prefix_dialog.setOption(QFileDialog.Option.ShowDirsOnly) @@ -96,7 +91,7 @@ def is_valid(self) -> bool: except KeyError: return False - def do_login(self): + def do_login(self) -> None: self.ui.status_field.setText(self.tr("Loading...")) if os.name != "nt": self.logger.info("Using EGL appdata path at %s", {self.egl_appdata}) diff --git a/rare/components/dialogs/login/login_frame.py b/rare/components/dialogs/login/login_frame.py new file mode 100644 index 0000000000..2325efaf6b --- /dev/null +++ b/rare/components/dialogs/login/login_frame.py @@ -0,0 +1,33 @@ +from abc import abstractmethod +from logging import getLogger + +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QFrame + +from rare.lgndr.core import LegendaryCore + + +class LoginFrame(QFrame): + success = Signal() + validated = Signal(bool) + + def __init__(self, core: LegendaryCore, parent=None): + super(LoginFrame, self).__init__(parent=parent) + + self.logger = getLogger(type(self).__name__) + self.core = core + + self.setFrameStyle(QFrame.Shape.StyledPanel) + + @abstractmethod + def is_valid(self) -> bool: + pass + + @Slot() + def _on_input_changed(self): + self.validated.emit(self.is_valid()) + + @abstractmethod + def do_login(self) -> None: + pass + diff --git a/rare/components/main_window.py b/rare/components/main_window.py index 41fe9ee553..2f9a85c5a5 100644 --- a/rare/components/main_window.py +++ b/rare/components/main_window.py @@ -118,7 +118,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.tray_icon: TrayIcon = TrayIcon(self.settings, self.rcore, self) self.tray_icon.exit_app.connect(self.__on_exit_app) self.tray_icon.show_app.connect(self.show) - self.tray_icon.activated.connect(lambda r: self.toggle() if r == QSystemTrayIcon.ActivationReason.DoubleClick else None) + self.tray_icon.activated.connect(self._on_tray_icon_activated) # enable kinetic scrolling for scroll_area in self.findChildren(QScrollArea): @@ -132,6 +132,11 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): for combo_box in scroll_area.findChildren(QComboBox): combo_box.wheelEvent = lambda e: e.ignore() + @Slot(QSystemTrayIcon.ActivationReason) + def _on_tray_icon_activated(self, reason: QSystemTrayIcon.ActivationReason): + if reason == QSystemTrayIcon.ActivationReason.DoubleClick: + self.toggle() + def center_window(self): # get the margins of the decorated window margins = self.windowHandle().frameMargins() diff --git a/rare/components/tabs/__init__.py b/rare/components/tabs/__init__.py index 6af53e6d66..a8b97dcb97 100644 --- a/rare/components/tabs/__init__.py +++ b/rare/components/tabs/__init__.py @@ -62,11 +62,11 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent): # Settings Tab self.settings_tab = SettingsTab(settings, rcore, self) self.settings_index = self.addTab(self.settings_tab, qta_icon("fa.gear", "fa6s.gear"), self.tr("Settings")) - self.settings_tab.about.update_available_ready.connect(lambda: self.main_bar.setTabText(self.settings_index, self.tr("Settings (!)"))) + self.settings_tab.update_available.connect(self._on_update_available) # Account Tab self.account_widget = AccountWidget(self.signals, self.core, self) - self.account_widget.exit_app.connect(self.__on_exit_app) + self.account_widget.exit_app.connect(self._on_exit_app) account_action = QWidgetAction(self) account_action.setDefaultWidget(self.account_widget) self.account_menu = QMenu(self) @@ -80,12 +80,12 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent): self.tabBarClicked.connect(self.mouse_clicked) # shortcuts - QShortcut("Alt+1", self).activated.connect(lambda: self.setCurrentIndex(self.games_index)) + QShortcut("Alt+1", self).activated.connect(self._on_shortcut_activated_games) if not self.args.offline: - QShortcut("Alt+2", self).activated.connect(lambda: self.setCurrentIndex(self.downloads_index)) - QShortcut("Alt+3", self).activated.connect(lambda: self.setCurrentIndex(self.store_index)) - QShortcut("Alt+4", self).activated.connect(lambda: self.setCurrentIndex(self.integrations_index)) - QShortcut("Alt+5", self).activated.connect(lambda: self.setCurrentIndex(self.settings_index)) + QShortcut("Alt+2", self).activated.connect(self._on_shortcut_activated_downloads) + QShortcut("Alt+3", self).activated.connect(self._on_shortcut_activated_store) + QShortcut("Alt+4", self).activated.connect(self._on_shortcut_activated_integrations) + QShortcut("Alt+5", self).activated.connect(self._on_shortcut_activated_settings) self.setCurrentIndex(self.games_index) @@ -104,6 +104,30 @@ def eventFilter(self, w: QObject, e: QEvent) -> bool: return True return False + @Slot() + def _on_shortcut_activated_games(self): + self.setCurrentIndex(self.games_index) + + @Slot() + def _on_shortcut_activated_downloads(self): + self.setCurrentIndex(self.downloads_index) + + @Slot() + def _on_shortcut_activated_store(self): + self.setCurrentIndex(self.store_index) + + @Slot() + def _on_shortcut_activated_integrations(self): + self.setCurrentIndex(self.integrations_index) + + @Slot() + def _on_shortcut_activated_settings(self): + self.setCurrentIndex(self.settings_index) + + @Slot() + def _on_update_available(self): + self.main_bar.setTabText(self.settings_index, self.tr("Settings (!)")) + @Slot() @Slot(str) def show_import(self, app_name: str = None): @@ -144,7 +168,7 @@ def resizeEvent(self, event): super(MainTabWidget, self).resizeEvent(event) @Slot(int) - def __on_exit_app(self, exit_code: int): + def _on_exit_app(self, exit_code: int): # FIXME: Don't allow logging out if there are active downloads if self.downloads_tab.is_download_active: QMessageBox.warning( diff --git a/rare/components/tabs/account/__init__.py b/rare/components/tabs/account/__init__.py index e85337ed37..94326552b4 100644 --- a/rare/components/tabs/account/__init__.py +++ b/rare/components/tabs/account/__init__.py @@ -26,13 +26,12 @@ def __init__(self, signals: GlobalSignals, core: LegendaryCore, parent): qta_icon("fa.external-link", "fa5s.external-link-alt"), self.tr("Account settings"), ) - self.open_browser.clicked.connect( - lambda: webbrowser.open("https://www.epicgames.com/account/personal?productName=epicgames") - ) + self.open_browser.clicked.connect(self._on_browser_clicked) + self.logout_button = QPushButton(self.tr("Logout"), parent=self) - self.logout_button.clicked.connect(self.__on_logout) + self.logout_button.clicked.connect(self._on_logout) self.quit_button = QPushButton(self.tr("Quit"), parent=self) - self.quit_button.clicked.connect(self.__on_quit) + self.quit_button.clicked.connect(self._on_quit) layout = QVBoxLayout(self) layout.addWidget(QLabel(self.tr("Logged in as {}").format(username))) @@ -43,9 +42,13 @@ def __init__(self, signals: GlobalSignals, core: LegendaryCore, parent): layout.addWidget(self.quit_button) @Slot() - def __on_quit(self): + def _on_browser_clicked(self): + webbrowser.open("https://www.epicgames.com/account/personal?productName=epicgames") + + @Slot() + def _on_quit(self): self.exit_app.emit(ExitCodes.EXIT) @Slot() - def __on_logout(self): + def _on_logout(self): self.exit_app.emit(ExitCodes.LOGOUT) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index a42e8c7adf..12857903a3 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -34,8 +34,6 @@ from .groups import QueueGroup, UpdateGroup from .thread import DlResultCode, DlResultModel, DlThread -logger = getLogger("Download") - def get_time(seconds: Union[int, float]) -> str: return str(datetime.timedelta(seconds=seconds)) @@ -47,6 +45,8 @@ class DownloadsTab(QWidget): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): super(DownloadsTab, self).__init__(parent=parent) + self.logger = getLogger(type(self).__name__) + self.settings = settings self.rcore = rcore self.core = rcore.core() @@ -158,7 +158,7 @@ def __on_queue_force(self, item: InstallQueueItemModel): self.stop_download() self.__forced_item = item else: - self.__start_download(item) + self.start_download(item) def stop_download(self, omit_queue=False): """ @@ -181,17 +181,26 @@ def stop_download(self, omit_queue=False): def __refresh_download(self, item: InstallQueueItemModel): worker = InstallInfoWorker(self.core, item.options) - worker.signals.result.connect(lambda d: self.__start_download(InstallQueueItemModel(options=item.options, download=d))) + + worker.signals.result.connect( + (lambda obj, d: obj.start_download( + InstallQueueItemModel(options=item.options, download=d))).__get__(self) + ) worker.signals.failed.connect( - lambda m: logger.error(f"Failed to refresh download for {item.options.app_name} with error: {m}") + (lambda obj, m: obj.logger.error( + f"Failed to refresh download for {item.options.app_name} with error: {m}")).__get__(self) + ) + worker.signals.finished.connect( + (lambda obj, m: obj.logger.info( + f"Download refresh worker finished for {item.options.app_name}")).__get__(self) ) - worker.signals.finished.connect(lambda: logger.info(f"Download refresh worker finished for {item.options.app_name}")) + QThreadPool.globalInstance().start(worker) - def __start_download(self, item: InstallQueueItemModel): + def start_download(self, item: InstallQueueItemModel): rgame = self.rcore.get_game(item.options.app_name) if not rgame.state == RareGame.State.DOWNLOADING: - logger.error( + self.logger.error( f"Can't start download {item.options.app_name} due to incompatible state {RareGame.State(rgame.state).name}" ) # lk: invalidate the queue item in case the game was uninstalled @@ -228,7 +237,7 @@ def __requeue_download(self, item: InstallQueueItemModel): rgame = self.rcore.get_game(item.options.app_name) rgame.state = RareGame.State.DOWNLOADING self.queue_group.push_front(item, rgame.igame) - logger.info(f"Re-queued download for {rgame.app_name} ({rgame.app_title})") + self.logger.info(f"Re-queued download for {rgame.app_name} ({rgame.app_title})") @Slot(DlResultModel) def __on_download_result(self, result: DlResultModel): @@ -237,7 +246,7 @@ def __on_download_result(self, result: DlResultModel): self.__thread.deleteLater() if result.code == DlResultCode.FINISHED: - logger.info(f"Download finished: {result.options.app_name}") + self.logger.info(f"Download finished: {result.options.app_name}") if result.shortcut and desktop_links_supported(): if not create_desktop_link( app_name=result.options.app_name, @@ -246,9 +255,9 @@ def __on_download_result(self, result: DlResultModel): link_type="desktop", ): # maybe add it to download summary, to show in finished downloads - logger.error(f"Failed to create desktop link on {platform.system()}") + self.logger.error(f"Failed to create desktop link on {platform.system()}") else: - logger.info(f"Created desktop link {result.folder_name} for {result.app_title}") + self.logger.info(f"Created desktop link {result.folder_name} for {result.app_title}") self.signals.application.notify.emit( self.tr("Downloads"), @@ -259,7 +268,7 @@ def __on_download_result(self, result: DlResultModel): self.updates_group.set_widget_enabled(result.options.app_name, True) elif result.code == DlResultCode.ERROR: - logger.error(f"Download error: {result.options.app_name} ({result.message})") + self.logger.error(f"Download error: {result.options.app_name} ({result.message})") QMessageBox.warning( self, self.tr("Error - {}").format(result.app_title), @@ -268,7 +277,7 @@ def __on_download_result(self, result: DlResultModel): ) elif result.code == DlResultCode.STOPPED: - logger.info(f"Download stopped: {result.options.app_name}") + self.logger.info(f"Download stopped: {result.options.app_name}") if not self.__omit_requeue: self.__requeue_download(InstallQueueItemModel(options=result.options)) else: @@ -279,9 +288,9 @@ def __on_download_result(self, result: DlResultModel): self.updates_group.set_widget_enabled(result.options.app_name, True) if result.code == DlResultCode.FINISHED and self.queue_group.count(): - self.__start_download(self.queue_group.pop_front()) + self.start_download(self.queue_group.pop_front()) elif result.code == DlResultCode.STOPPED and self.__forced_item: - self.__start_download(self.__forced_item) + self.start_download(self.__forced_item) self.__forced_item = None else: self.__reset_download() @@ -321,7 +330,7 @@ def __on_install_dialog_closed(self, item: InstallQueueItemModel): if item: # lk: start update only if there is no other active thread and there is no queue if self.__thread is None and not self.queue_group.count(): - self.__start_download(item) + self.start_download(item) else: rgame = self.rcore.get_game(item.options.app_name) self.queue_group.push_back(item, rgame.igame) diff --git a/rare/components/tabs/downloads/groups.py b/rare/components/tabs/downloads/groups.py index ebbd0700ca..6fd185e993 100644 --- a/rare/components/tabs/downloads/groups.py +++ b/rare/components/tabs/downloads/groups.py @@ -66,6 +66,7 @@ def append(self, game: Game, igame: InstalledGame): widget: UpdateWidget = self.__find_widget(game.app_name) if widget is not None: self.__container.layout().removeWidget(widget) + widget.disconnect(widget) widget.deleteLater() widget = UpdateWidget(self.imgmgr, game, igame, parent=self.__container) widget.destroyed.connect(self.__update_group) @@ -75,6 +76,7 @@ def append(self, game: Game, igame: InstalledGame): def remove(self, app_name: str): widget: UpdateWidget = self.__find_widget(app_name) self.__container.layout().removeWidget(widget) + widget.disconnect(widget) widget.deleteLater() def set_widget_enabled(self, app_name: str, enabled: bool): @@ -191,6 +193,7 @@ def __remove(self, app_name: str): self.__queue.remove(app_name) widget: QueueWidget = self.__find_widget(app_name) self.__container.layout().removeWidget(widget) + widget.disconnect(widget) widget.deleteLater() @Slot(str) diff --git a/rare/components/tabs/downloads/thread.py b/rare/components/tabs/downloads/thread.py index ec797dd931..3a4386c40b 100644 --- a/rare/components/tabs/downloads/thread.py +++ b/rare/components/tabs/downloads/thread.py @@ -58,16 +58,17 @@ def __init__( self.rgame = rgame self.debug = debug - def __finish(self, result): + def _finish(self, result): if result.code == DlResultCode.FINISHED and not result.options.no_install: self.rgame.set_installed(True) self.rgame.state = RareGame.State.IDLE self.rgame.signals.progress.finish.emit(result.code != DlResultCode.FINISHED) self.result.emit(result) + self.quit() - def __status_callback(self, status: UIUpdate): + def _status_callback(self, status: UIUpdate): self.progress.emit(status, self.dl_size) - self.rgame.signals.progress.update.emit(int(status.progress)) + self.rgame.signals.progress.refresh.emit(int(status.progress)) def run(self): cli = LegendaryCLI(self.core) @@ -115,7 +116,7 @@ def chunk_url_sign_thread(): time.sleep(1) while self.item.download.dlm.is_alive(): try: - self.__status_callback(self.item.download.dlm.status_queue.get(timeout=1.0)) + self._status_callback(self.item.download.dlm.status_queue.get(timeout=1.0)) except queue.Empty: pass if self.dlm_signals.update: @@ -194,7 +195,7 @@ def chunk_url_sign_thread(): sign_thread.stop = True ticket_thread.join() sign_thread.join() - self.__finish(result) + self._finish(result) def _handle_postinstall(self, postinstall, igame): self.logger.info("This game lists the following prerequisites to be installed:") @@ -209,7 +210,8 @@ def _handle_postinstall(self, postinstall, igame): proc = QProcess(self) proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) proc.readyReadStandardOutput.connect( - lambda: self.logger.debug(str(proc.readAllStandardOutput().data(), "utf-8", "ignore")) + (lambda obj: obj.logger.debug( + str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))).__get__(self) ) proc.setProgram(fullpath) proc.setArguments(postinstall.get("args", "").split(" ")) diff --git a/rare/components/tabs/downloads/widgets.py b/rare/components/tabs/downloads/widgets.py index 00f67f0987..93f32f4f80 100644 --- a/rare/components/tabs/downloads/widgets.py +++ b/rare/components/tabs/downloads/widgets.py @@ -90,10 +90,18 @@ def __init__(self, imgmgr: ImageManager, game: Game, igame: InstalledGame, paren self.info_widget = QueueInfoWidget(imgmgr, game, igame, parent=self) self.ui.info_layout.addWidget(self.info_widget) - self.ui.update_button.clicked.connect(lambda: self.update_game(True)) - self.ui.settings_button.clicked.connect(lambda: self.update_game(False)) + self.ui.update_button.clicked.connect(self._on_update_clicked) + self.ui.settings_button.clicked.connect(self._on_settings_clicked) - def update_game(self, auto: bool): + @Slot() + def _on_update_clicked(self): + self._update_game(True) + + @Slot() + def _on_settings_clicked(self): + self._update_game(False) + + def _update_game(self, auto: bool): self.ui.update_button.setDisabled(True) self.ui.settings_button.setDisabled(True) self.enqueue.emit(InstallOptionsModel( @@ -126,6 +134,7 @@ def __init__( self, core: LegendaryCore, imgmgr: ImageManager, item: InstallQueueItemModel, old_igame: InstalledGame, parent=None ): super(QueueWidget, self).__init__(parent=parent) + self.logger = getLogger(type(self).__name__) self.ui = Ui_QueueBaseWidget() self.ui.setupUi(self) # lk: setObjectName has to be after `setupUi` because it is also set in that function @@ -136,10 +145,14 @@ def __init__( worker = InstallInfoWorker(core, item.options) worker.signals.result.connect(self.__update_info) worker.signals.failed.connect( - lambda m: logger.error(f"Failed to requeue download for {item.options.app_name} with error: {m}") + (lambda obj, m: obj.logger.error( + f"Failed to requeue download for {item.options.app_name} with error: {m}")).__get__(self) + ) + worker.signals.failed.connect((lambda obj, m: obj.remove.emit(item.options.app_name)).__get__(self)) + worker.signals.finished.connect( + (lambda obj: obj.logger.error( + f"Download requeue worker finished for {item.options.app_name}")).__get__(self) ) - worker.signals.failed.connect(lambda m: self.remove.emit(item.options.app_name)) - worker.signals.finished.connect(lambda: logger.info(f"Download requeue worker finished for {item.options.app_name}")) QThreadPool.globalInstance().start(worker) self.info_widget = QueueInfoWidget(imgmgr, None, None, None, old_igame, parent=self) else: @@ -158,13 +171,29 @@ def __init__( self.item = item self.ui.move_up_button.setIcon(qta_icon("fa.arrow-up", "fa5s.arrow-up")) - self.ui.move_up_button.clicked.connect(lambda: self.move_up.emit(self.item.options.app_name)) + self.ui.move_up_button.clicked.connect(self._on_move_up) self.ui.move_down_button.setIcon(qta_icon("fa.arrow-down", "fa5s.arrow-down")) - self.ui.move_down_button.clicked.connect(lambda: self.move_down.emit(self.item.options.app_name)) + self.ui.move_down_button.clicked.connect(self._on_move_down) + + self.ui.remove_button.clicked.connect(self._on_remove) + self.ui.force_button.clicked.connect(self._on_force) + + @Slot() + def _on_move_up(self): + self.move_up.emit(self.item.options.app_name) + + @Slot() + def _on_move_down(self): + self.move_down.emit(self.item.options.app_name) + + @Slot() + def _on_remove(self): + self.remove.emit(self.item.options.app_name) - self.ui.remove_button.clicked.connect(lambda: self.remove.emit(self.item.options.app_name)) - self.ui.force_button.clicked.connect(lambda: self.force.emit(self.item)) + @Slot() + def _on_force(self): + self.force.emit(self.item) @Slot(InstallDownloadModel) def __update_info(self, download: InstallDownloadModel): diff --git a/rare/components/tabs/integrations/egl_sync_group.py b/rare/components/tabs/integrations/egl_sync_group.py index 6954e67130..2f467b4790 100644 --- a/rare/components/tabs/integrations/egl_sync_group.py +++ b/rare/components/tabs/integrations/egl_sync_group.py @@ -265,21 +265,32 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.setupUi(self) self.ui.list.setFrameShape(QFrame.Shape.NoFrame) - self.ui.list.itemDoubleClicked.connect( - lambda item: item.setCheckState(Qt.CheckState.Unchecked) - if item.checkState() != Qt.CheckState.Unchecked - else item.setCheckState(Qt.CheckState.Checked) - ) - self.ui.list.itemChanged.connect(self.has_selected) + self.ui.list.itemDoubleClicked.connect(self._on_item_double_clicked) + self.ui.list.itemChanged.connect(self._has_selected) - self.ui.select_all_button.clicked.connect(lambda: self.mark(Qt.CheckState.Checked)) - self.ui.select_none_button.clicked.connect(lambda: self.mark(Qt.CheckState.Unchecked)) + self.ui.select_all_button.clicked.connect(self._on_mark_all) + self.ui.select_none_button.clicked.connect(self._on_mark_none) self.ui.action_button.clicked.connect(self.action) self.action_errors.connect(self.show_errors) - def has_selected(self): + @Slot() + def _on_mark_all(self): + self.mark(Qt.CheckState.Checked) + + @Slot() + def _on_mark_none(self): + self.mark(Qt.CheckState.Unchecked) + + @Slot(QListWidgetItem) + def _on_item_double_clicked(self, item: QListWidgetItem): + if item.checkState() != Qt.CheckState.Unchecked: + item.setCheckState(Qt.CheckState.Unchecked) + else: + item.setCheckState(Qt.CheckState.Checked) + + def _has_selected(self): for item in self.items: if item.is_checked(): self.ui.action_button.setEnabled(True) diff --git a/rare/components/tabs/integrations/eos_group.py b/rare/components/tabs/integrations/eos_group.py index b9f500ba42..8c7940c7b9 100644 --- a/rare/components/tabs/integrations/eos_group.py +++ b/rare/components/tabs/integrations/eos_group.py @@ -37,19 +37,23 @@ logger = getLogger("EpicOverlay") +class CheckForUpdateWorkerSignals(QObject): + update_available = Signal(bool) + + class CheckForUpdateWorker(QRunnable): - class CheckForUpdateSignals(QObject): - update_available = Signal(bool) def __init__(self, core: LegendaryCore): super(CheckForUpdateWorker, self).__init__() - self.signals = self.CheckForUpdateSignals() + self.signals = CheckForUpdateWorkerSignals() self.setAutoDelete(True) self.core = core def run(self) -> None: self.core.check_for_overlay_updates() self.signals.update_available.emit(self.core.overlay_update_available) + self.signals.disconnect(self.signals) + self.signals.deleteLater() class EosPrefixWidget(QFrame): @@ -199,7 +203,7 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.info_layout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.version) self.ui.info_layout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.install_path) - self.overlay.signals.widget.update.connect(self.update_state) + self.overlay.signals.widget.refresh.connect(self.update_state) self.overlay.signals.game.installed.connect(self.install_finished) self.overlay.signals.game.uninstalled.connect(self.uninstall_finished) @@ -222,6 +226,7 @@ def hideEvent(self, e: QHideEvent, /): if e.spontaneous(): return super().hideEvent(e) for widget in self.findChildren(EosPrefixWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): + widget.disconnect(widget) widget.deleteLater() return super().hideEvent(e) @@ -249,6 +254,7 @@ def update_state(self): def update_prefixes(self): for widget in self.findChildren(EosPrefixWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): + widget.disconnect(widget) widget.deleteLater() if platform.system() != "Windows": diff --git a/rare/components/tabs/integrations/import_group.py b/rare/components/tabs/integrations/import_group.py index 29a7c662ab..7d9ee0f8c1 100644 --- a/rare/components/tabs/integrations/import_group.py +++ b/rare/components/tabs/integrations/import_group.py @@ -67,10 +67,12 @@ class ImportedGame: message: Optional[str] = None +class ImportWorkerSignals(QObject): + progress = Signal(ImportedGame, int) + result = Signal(list) + + class ImportWorker(QRunnable): - class Signals(QObject): - progress = Signal(ImportedGame, int) - result = Signal(list) def __init__( self, @@ -84,7 +86,7 @@ def __init__( ): super(ImportWorker, self).__init__() self.setAutoDelete(True) - self.signals = ImportWorker.Signals() + self.signals = ImportWorkerSignals() self.core = core self.path = Path(path) @@ -110,6 +112,8 @@ def run(self) -> None: result_list.append(result) self.signals.progress.emit(result, 100) self.signals.result.emit(result_list) + self.signals.disconnect(self.signals) + self.signals.deleteLater() def _try_import(self, path: Path, app_name: str = None) -> ImportedGame: result = ImportedGame(ImportResult.ERROR) @@ -188,13 +192,13 @@ def __init__(self, rcore: RareCore, parent=None): self.app_name_edit, ) - self.ui.import_folder_check.checkStateChanged.connect(self.import_folder_changed) + self.ui.import_folder_check.checkStateChanged.connect(self._on_import_folder_changed) self.ui.import_dlcs_check.setEnabled(False) - self.ui.import_dlcs_check.checkStateChanged.connect(self.import_dlcs_changed) + self.ui.import_dlcs_check.checkStateChanged.connect(self._on_import_dlcs_changed) self.ui.import_button_label.setText("") self.ui.import_button.setEnabled(False) - self.ui.import_button.clicked.connect(lambda: self._import(self.path_edit.text())) + self.ui.import_button.clicked.connect(self._on_import_clicked) self.button_info_stack = QStackedWidget(self) self.button_info_stack.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) @@ -276,7 +280,7 @@ def _app_name_changed(self, app_name: str): self.ui.import_button.setEnabled(not bool(self.worker) and (self.app_name_edit.is_valid and self.path_edit.is_valid)) @Slot(Qt.CheckState) - def import_folder_changed(self, state: Qt.CheckState): + def _on_import_folder_changed(self, state: Qt.CheckState): self.app_name_edit.setEnabled(not state) self.ui.platform_combo.setEnabled(not state) self.ui.platform_combo.setToolTip( @@ -300,11 +304,15 @@ def import_folder_changed(self, state: Qt.CheckState): ) @Slot(Qt.CheckState) - def import_dlcs_changed(self, state: Qt.CheckState): + def _on_import_dlcs_changed(self, state: Qt.CheckState): self.ui.import_button.setEnabled( not bool(self.worker) and (state != Qt.CheckState.Unchecked or self.app_name_edit.is_valid) ) + @Slot() + def _on_import_clicked(self): + self._import(self.path_edit.text()) + @Slot(str) def _import(self, path: Optional[str] = None): self.ui.import_button.setDisabled(True) diff --git a/rare/components/tabs/integrations/ubisoft_group.py b/rare/components/tabs/integrations/ubisoft_group.py index 2a3f8b587f..fd772dd26d 100644 --- a/rare/components/tabs/integrations/ubisoft_group.py +++ b/rare/components/tabs/integrations/ubisoft_group.py @@ -25,7 +25,7 @@ class UbiGetInfoWorkerSignals(QObject): - worker_finished = Signal(set, set, str) + result = Signal(set, set, str) class UbiGetInfoWorker(Worker): @@ -45,7 +45,7 @@ def run_real(self) -> None: ubi_account_id = ext_auth["externalAuthId"] break else: - self.signals.worker_finished.emit(set(), set(), "") + self.signals.result.emit(set(), set(), "") return with timelogger(self.logger, "Request uplay codes"): @@ -62,10 +62,10 @@ def run_real(self) -> None: self.core.lgd.entitlements = entitlements entitlements = {i["entitlementName"] for i in entitlements} - self.signals.worker_finished.emit(redeemed, entitlements, ubi_account_id) + self.signals.result.emit(redeemed, entitlements, ubi_account_id) except Exception as e: self.logger.error(e) - self.signals.worker_finished.emit(set(), set(), "error") + self.signals.result.emit(set(), set(), "error") class UbiConnectWorkerSignals(QObject): @@ -138,10 +138,10 @@ def activate(self): worker = UbiConnectWorker(self.core, None, None) else: worker = UbiConnectWorker(self.core, self.ubi_account_id, self.game.partner_link_id) - worker.signals.linked.connect(self.worker_finished) + worker.signals.linked.connect(self._on_linked) QThreadPool.globalInstance().start(worker) - def worker_finished(self, error): + def _on_linked(self, error): if not error: self.redeem_indicator.setPixmap( qta_icon("fa.check-circle-o", "fa5.check-circle", color="green").pixmap(QSize(20, 20)) @@ -173,7 +173,7 @@ def __init__(self, rcore: RareCore, parent=None): self.info_label.setText(self.tr("Getting information about your redeemable Ubisoft games.")) self.link_button = QPushButton(self.tr("Link Ubisoft acccount"), parent=self) self.link_button.setMinimumWidth(140) - self.link_button.clicked.connect(lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft")) + self.link_button.clicked.connect(self._on_link_clicked) self.link_button.setEnabled(False) self.loading_widget = LoadingWidget(self) @@ -187,6 +187,10 @@ def __init__(self, rcore: RareCore, parent=None): layout.addLayout(header_layout) layout.addWidget(self.loading_widget) + @Slot() + def _on_link_clicked(self): + webbrowser.open("https://www.epicgames.com/id/link/ubisoft") + def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) @@ -195,16 +199,17 @@ def showEvent(self, a0: QShowEvent) -> None: return super().showEvent(a0) for widget in self.findChildren(UbiLinkWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): + widget.disconnect(widget) widget.deleteLater() self.loading_widget.start() self.worker = UbiGetInfoWorker(self.core) - self.worker.signals.worker_finished.connect(self.show_ubi_games) + self.worker.signals.result.connect(self._on_result) self.thread_pool.start(self.worker) return super().showEvent(a0) @Slot(set, set, str) - def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str): + def _on_result(self, redeemed: set, entitlements: set, ubi_account_id: str): self.worker = None self.loading_widget.stop() if not redeemed and ubi_account_id != "error": diff --git a/rare/components/tabs/library/__init__.py b/rare/components/tabs/library/__init__.py index 1d7ebea27e..5d515d1a41 100644 --- a/rare/components/tabs/library/__init__.py +++ b/rare/components/tabs/library/__init__.py @@ -42,9 +42,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): library_page_right_layout.addWidget(self.head_bar) self.details_page = GameDetailsTabs(settings, rcore, self) - self.details_page.back_clicked.connect(lambda: self.setCurrentWidget(self.library_page)) - # Update visibility of hidden games - self.details_page.back_clicked.connect(lambda: self.filter_games(self.head_bar.current_filter())) + self.details_page.back_clicked.connect(self._on_back_clicked) self.details_page.import_clicked.connect(self.import_clicked) self.addWidget(self.details_page) @@ -59,11 +57,11 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.library_controller = LibraryWidgetController(rcore, library_view, self.view_scroll) self.head_bar.search_bar.textChanged.connect(self.search_games) - self.head_bar.search_bar.textChanged.connect(self.scroll_to_top) + self.head_bar.search_bar.textChanged.connect(self._scroll_to_top) self.head_bar.filterChanged.connect(self.filter_games) - self.head_bar.filterChanged.connect(self.scroll_to_top) + self.head_bar.filterChanged.connect(self._scroll_to_top) self.head_bar.orderChanged.connect(self.order_games) - self.head_bar.orderChanged.connect(self.scroll_to_top) + self.head_bar.orderChanged.connect(self._scroll_to_top) # signals self.signals.game.installed.connect(self.update_count_games_label) @@ -79,7 +77,12 @@ def showEvent(self, a0: QShowEvent): return super().showEvent(a0) @Slot() - def scroll_to_top(self): + def _on_back_clicked(self): + self.filter_games(self.head_bar.current_filter()) + self.setCurrentWidget(self.library_page) + + @Slot() + def _scroll_to_top(self): self.view_scroll.verticalScrollBar().setSliderPosition(self.view_scroll.verticalScrollBar().minimum()) @Slot() diff --git a/rare/components/tabs/library/details/cloud_saves.py b/rare/components/tabs/library/details/cloud_saves.py index 694e1fec53..2dfe6d326a 100644 --- a/rare/components/tabs/library/details/cloud_saves.py +++ b/rare/components/tabs/library/details/cloud_saves.py @@ -234,12 +234,12 @@ def __update_widget(self): def update_game(self, rgame: RareGame): if self.rgame: - self.rgame.signals.widget.update.disconnect(self.__update_widget) + self.rgame.signals.widget.refresh.disconnect(self.__update_widget) self.rgame = rgame self.save_path_spec = PathSpec(self.core, self.rgame.igame).resolve_egl_path_vars(self.rgame.raw_save_path) self.set_title.emit(rgame.app_title) - rgame.signals.widget.update.connect(self.__update_widget) + rgame.signals.widget.refresh.connect(self.__update_widget) self.__update_widget() diff --git a/rare/components/tabs/library/details/details.py b/rare/components/tabs/library/details/details.py index 32568d2609..1b56f66d36 100644 --- a/rare/components/tabs/library/details/details.py +++ b/rare/components/tabs/library/details/details.py @@ -395,6 +395,7 @@ def __update_widget(self): self.ui.actions_stack.setCurrentWidget(self.ui.uninstalled_page) for w in self.ui.tags_group.findChildren(GameTagCheckBox, options=Qt.FindChildOption.FindDirectChildrenOnly): + w.disconnect(w) w.deleteLater() for tag in self.rcore.game_tags: @@ -417,11 +418,11 @@ def update_game(self, rgame: RareGame): worker.signals.progress.disconnect(self.__on_move_progress) except TypeError as e: logger.warning(f"{self.rgame.app_name} move worker: {e}") - self.rgame.signals.widget.update.disconnect(self.__update_widget) + self.rgame.signals.widget.refresh.disconnect(self.__update_widget) self.rgame = None - rgame.signals.widget.update.connect(self.__update_widget) + rgame.signals.widget.refresh.connect(self.__update_widget) if (worker := rgame.get_worker()) is not None: if isinstance(worker, VerifyWorker): worker.signals.progress.connect(self.__on_verify_progress) @@ -445,6 +446,7 @@ def update_game(self, rgame: RareGame): ): for w in page.findChildren(AchievementWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): page.layout().removeWidget(w) + w.disconnect(w) w.deleteLater() if ach := rgame.achievements: @@ -525,11 +527,15 @@ def __init__(self, tag: str, parent=None): ((base_color & 0xFF0000) >> 16) * 0.2126 + ((base_color & 0x00FF00) >> 8) * 0.7152 + (base_color & 0x0000FF) * 0.0722 ) font_color = "white" if luminance < 140 else "black" - style = ("QCheckBox#{0}{{color: {1};border-color: #{2:x};background-color: #{3:x};}}").format( + style = "QCheckBox#{0}{{color: {1};border-color: #{2:x};background-color: #{3:x};}}".format( self.objectName(), font_color, border_color, base_color ) self.setStyleSheet(style) - self.checkStateChanged.connect(lambda state: self.checkStateChangedData.emit(state, self.tag)) + self.checkStateChanged.connect(self._on_state_changed) + + @Slot(Qt.CheckState) + def _on_state_changed(self, state: Qt.CheckState): + self.checkStateChangedData.emit(state, self.tag) def setText(self, text, /): fm = QFontMetrics(self.font()) diff --git a/rare/components/tabs/library/details/dlcs.py b/rare/components/tabs/library/details/dlcs.py index 638abc58d8..12a6f1bb40 100644 --- a/rare/components/tabs/library/details/dlcs.py +++ b/rare/components/tabs/library/details/dlcs.py @@ -33,7 +33,7 @@ def __init__(self, rgame: RareGame, rdlc: RareGame, parent=None): # self.image.setPixmap(rdlc.get_pixmap_icon(rdlc.is_installed)) self.__update() - rdlc.signals.widget.update.connect(self.__update) + rdlc.signals.widget.refresh.connect(self.__update) @Slot() def __update(self): @@ -153,6 +153,7 @@ def append_installed(self, rdlc: RareGame): a_widget: AvailableGameDlcWidget = self.get_available(rdlc.app_name) if a_widget is not None: self.ui.available_dlc_container.layout().removeWidget(a_widget) + a_widget.disconnect(a_widget) a_widget.deleteLater() i_widget: InstalledGameDlcWidget = InstalledGameDlcWidget(self.rgame, rdlc, self.ui.installed_dlc_container) i_widget.destroyed.connect(self.update_installed_page) @@ -177,10 +178,12 @@ def update_dlcs(self, rgame: RareGame): for i_widget in self.list_installed(): self.ui.installed_dlc_container.layout().removeWidget(i_widget) + i_widget.disconnect(i_widget) i_widget.deleteLater() for a_widget in self.list_available(): self.ui.available_dlc_container.layout().removeWidget(a_widget) + a_widget.disconnect(a_widget) a_widget.deleteLater() for dlc in sorted(self.rgame.owned_dlcs, key=lambda x: x.app_title): diff --git a/rare/components/tabs/library/widgets/game_widget.py b/rare/components/tabs/library/widgets/game_widget.py index 437af65677..36e23e5d2e 100644 --- a/rare/components/tabs/library/widgets/game_widget.py +++ b/rare/components/tabs/library/widgets/game_widget.py @@ -45,15 +45,13 @@ def __init__(self, rgame: RareGame, parent=None): self.install_action.triggered.connect(self._install) self.desktop_link_action = QAction(self) - self.desktop_link_action.triggered.connect(lambda: self._create_link(self.rgame.folder_name, "desktop")) + self.desktop_link_action.triggered.connect(self._create_link_desktop) self.menu_link_action = QAction(self) - self.menu_link_action.triggered.connect(lambda: self._create_link(self.rgame.folder_name, "start_menu")) + self.menu_link_action.triggered.connect(self._create_link_start_menu) self.steam_shortcut_action = QAction(self) - self.steam_shortcut_action.triggered.connect( - lambda: self._create_steam_shortcut(self.rgame.app_name, self.rgame.app_title) - ) + self.steam_shortcut_action.triggered.connect(self._create_steam_shortcut) self.reload_action = QAction(self.tr("Reload Image"), self) self.reload_action.triggered.connect(self._on_reload_image) @@ -64,15 +62,15 @@ def __init__(self, rgame: RareGame, parent=None): self.update_actions() # signals - self.rgame.signals.widget.update.connect(self.update_pixmap) - self.rgame.signals.widget.update.connect(self.update_buttons) - self.rgame.signals.widget.update.connect(self.update_state) + self.rgame.signals.widget.refresh.connect(self.update_pixmap) + self.rgame.signals.widget.refresh.connect(self.update_buttons) + self.rgame.signals.widget.refresh.connect(self.update_state) self.rgame.signals.game.installed.connect(self.update_actions) self.rgame.signals.game.uninstalled.connect(self.update_actions) self.rgame.signals.progress.start.connect(self.start_progress) - self.rgame.signals.progress.update.connect(lambda p: self.updateProgress(p)) - self.rgame.signals.progress.finish.connect(lambda e: self.hideProgress(e)) + self.rgame.signals.progress.refresh.connect(self.updateProgress) + self.rgame.signals.progress.finish.connect(self.hideProgress) self.state_strings = { RareGame.State.IDLE: "", @@ -134,7 +132,7 @@ def timerEvent(self, a0): def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - super().showEvent(a0) + return super().showEvent(a0) @Slot() def update_state(self): @@ -270,6 +268,14 @@ def _install(self): def _uninstall(self): self.show_info.emit(self.rgame) + @Slot() + def _create_link_desktop(self): + self._create_link(self.rgame.folder_name, "desktop") + + @Slot() + def _create_link_start_menu(self): + self._create_link(self.rgame.folder_name, "start_menu") + @Slot(str, str) def _create_link(self, name: str, link_type: str): if not desktop_links_supported(): @@ -299,8 +305,9 @@ def _create_link(self, name: str, link_type: str): shortcut_path.unlink(missing_ok=True) self.update_actions() - @Slot(str, str) - def _create_steam_shortcut(self, app_name: str, app_title: str): + @Slot() + def _create_steam_shortcut(self): + app_name, app_title = self.rgame.app_name, self.rgame.app_title if steam_shortcut_exists(app_name): if shortcut := remove_steam_shortcut(app_name): remove_steam_coverart(shortcut) diff --git a/rare/components/tabs/library/widgets/library_widget.py b/rare/components/tabs/library/widgets/library_widget.py index 9d88a57526..f1a43ff34a 100644 --- a/rare/components/tabs/library/widgets/library_widget.py +++ b/rare/components/tabs/library/widgets/library_widget.py @@ -1,6 +1,6 @@ from typing import List, Optional, Tuple -from PySide6.QtCore import QEvent, QObject, Qt +from PySide6.QtCore import QEvent, QObject, Qt, Slot from PySide6.QtGui import ( QBrush, QColor, @@ -149,12 +149,14 @@ def showProgress(self, color_pm: QPixmap, gray_pm: QPixmap) -> None: self.progress_label.setVisible(True) self.updateProgress(0) + @Slot(int) def updateProgress(self, progress: int): self.progress_label.setText(f"{progress:02}%") if progress > self._progress: self._progress = progress self.setPixmap(self.progressPixmap(self._color_pixmap, self._gray_pixmap, progress)) + @Slot(bool) def hideProgress(self, stopped: bool): self._color_pixmap = None self._gray_pixmap = None diff --git a/rare/components/tabs/settings/__init__.py b/rare/components/tabs/settings/__init__.py index 466d9bc304..28463c705b 100644 --- a/rare/components/tabs/settings/__init__.py +++ b/rare/components/tabs/settings/__init__.py @@ -1,5 +1,7 @@ import platform as pf +from PySide6.QtCore import Signal, Slot + from rare.models.settings import RareAppSettings from rare.shared import RareCore from rare.widgets.side_tab import SideTabWidget @@ -13,6 +15,8 @@ class SettingsTab(SideTabWidget): + update_available = Signal() + def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): super(SettingsTab, self).__init__(parent=parent) @@ -32,10 +36,15 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.about = About(self) title = self.tr("About") self.about_index = self.addTab(self.about, title, title) - self.about.update_available_ready.connect(lambda: self.tabBar().setTabText(self.about_index, "About (!)")) + self.about.update_available.connect(self._on_update_available) + self.about.update_available.connect(self.update_available) if rcore.args().debug: title = self.tr("Debug") self.debug_index = self.addTab(DebugSettings(rcore.signals(), self), title, title) self.setCurrentIndex(self.rare_index) + + @Slot() + def _on_update_available(self): + self.tabBar().setTabText(self.about_index, "About (!)") \ No newline at end of file diff --git a/rare/components/tabs/settings/about.py b/rare/components/tabs/settings/about.py index ec901e3cda..6dc16fe18a 100644 --- a/rare/components/tabs/settings/about.py +++ b/rare/components/tabs/settings/about.py @@ -2,7 +2,7 @@ from logging import getLogger from typing import Tuple -from PySide6.QtCore import Signal +from PySide6.QtCore import Signal, Slot from PySide6.QtGui import QShowEvent from PySide6.QtWidgets import QWidget @@ -23,7 +23,7 @@ def versiontuple(v) -> Tuple[int, ...]: class About(QWidget): - update_available_ready = Signal() + update_available = Signal() def __init__(self, parent=None): super(About, self).__init__(parent=parent) @@ -33,38 +33,43 @@ def __init__(self, parent=None): self.ui.version.setText(f"{__version__} {__codename__}") self.ui.update_label.setEnabled(False) - self.ui.update_lbl.setEnabled(False) + self.ui.update_field.setEnabled(False) self.ui.open_browser.setVisible(False) self.ui.open_browser.setEnabled(False) self.releases_url = "https://api.github.com/repos/RareDevs/Rare/releases/latest" self.manager = QtRequests(parent=self) - self.manager.get(self.releases_url, self.update_available_finished) + self.manager.get(self.releases_url, self._on_update_check_finished) - self.ui.open_browser.clicked.connect(lambda: webbrowser.open("https://github.com/RareDevs/Rare/releases/latest")) + self.ui.open_browser.clicked.connect(self._on_browser_clicked) - self.update_available = False + self._update_available = False def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - self.manager.get(self.releases_url, self.update_available_finished) - super().showEvent(a0) + self.manager.get(self.releases_url, self._on_update_check_finished) + return super().showEvent(a0) - def update_available_finished(self, data: dict): + @Slot() + def _on_browser_clicked(self): + webbrowser.open("https://github.com/RareDevs/Rare/releases/latest") + + @Slot(dict) + def _on_update_check_finished(self, data: dict): if latest_tag := data.get("tag_name"): - self.update_available = versiontuple(latest_tag) > versiontuple(__version__) + self._update_available = versiontuple(latest_tag) > versiontuple(__version__) else: - self.update_available = False + self._update_available = False - if self.update_available: + if self._update_available: logger.info(f"Update available: {__version__} -> {latest_tag}") - self.ui.update_lbl.setText(f"{__version__} -> {latest_tag}") - self.update_available_ready.emit() + self.ui.update_field.setText(f"{__version__} -> {latest_tag}") + self.update_available.emit() else: - self.ui.update_lbl.setText(self.tr("You have the latest version")) - self.ui.update_label.setEnabled(self.update_available) - self.ui.update_lbl.setEnabled(self.update_available) - self.ui.open_browser.setVisible(self.update_available) - self.ui.open_browser.setEnabled(self.update_available) + self.ui.update_field.setText(self.tr("You have the latest version")) + self.ui.update_label.setEnabled(self._update_available) + self.ui.update_field.setEnabled(self._update_available) + self.ui.open_browser.setVisible(self._update_available) + self.ui.open_browser.setEnabled(self._update_available) diff --git a/rare/components/tabs/settings/debug.py b/rare/components/tabs/settings/debug.py index bb1c3e20e5..08b02856e0 100644 --- a/rare/components/tabs/settings/debug.py +++ b/rare/components/tabs/settings/debug.py @@ -1,3 +1,4 @@ +from PySide6.QtCore import Slot from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget from rare.models.signals import GlobalSignals @@ -12,7 +13,7 @@ def __init__(self, signals: GlobalSignals, parent=None): self.raise_runtime_exception_button = QPushButton("Raise Exception", self) self.raise_runtime_exception_button.clicked.connect(self.raise_exception) self.restart_button = QPushButton("Restart", self) - self.restart_button.clicked.connect(lambda: self.signals.application.quit.emit(ExitCodes.LOGOUT)) + self.restart_button.clicked.connect(self._on_restart_clicked) self.send_notification_button = QPushButton("Notify", self) self.send_notification_button.clicked.connect(self.send_notification) @@ -22,6 +23,10 @@ def __init__(self, signals: GlobalSignals, parent=None): layout.addWidget(self.send_notification_button) layout.addStretch(1) + @Slot() + def _on_restart_clicked(self): + self.signals.application.quit.emit(ExitCodes.LOGOUT) + def raise_exception(self): raise RuntimeError("Debug Crash") diff --git a/rare/components/tabs/settings/legendary.py b/rare/components/tabs/settings/legendary.py index 69fc9a5250..1a865a306a 100644 --- a/rare/components/tabs/settings/legendary.py +++ b/rare/components/tabs/settings/legendary.py @@ -23,14 +23,16 @@ logger = getLogger("LegendarySettings") +class RefreshGameMetaWorkerSignals(QObject): + finished = Signal() + + class RefreshGameMetaWorker(Worker): - class Signals(QObject): - finished = Signal() def __init__(self, core: LegendaryCore, platforms: Set[str], include_unreal: bool): super(RefreshGameMetaWorker, self).__init__() self.core = core - self.signals = RefreshGameMetaWorker.Signals() + self.signals = RefreshGameMetaWorkerSignals() self.platforms = platforms if platforms else {"Windows"} self.skip_ue = not include_unreal @@ -57,7 +59,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): placeholder=self.tr("Default installation folder for macOS games"), file_mode=QFileDialog.FileMode.Directory, edit_func=self.__path_edit_callback, - save_func=self.__path_save_callback_mac, + save_func=self._path_save_callback_mac, ) self.ui.install_dir_layout.addWidget(self.mac_install_dir_edit) @@ -67,7 +69,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): placeholder=self.tr("Default installation folder for Windows games"), file_mode=QFileDialog.FileMode.Directory, edit_func=self.__path_edit_callback, - save_func=self.__path_save_callback_win, + save_func=self._path_save_callback_win, ) self.ui.install_dir_layout.addWidget(self.install_dir_edit) @@ -88,8 +90,8 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.disable_https_check.checkStateChanged.connect(self.disable_https_save) # Clean metadata - self.ui.clean_button.clicked.connect(lambda: self.clean_metadata(keep_manifests=False)) - self.ui.clean_keep_manifests_button.clicked.connect(lambda: self.clean_metadata(keep_manifests=True)) + self.ui.clean_button.clicked.connect(self._on_clean_clicked) + self.ui.clean_keep_manifests_button.clicked.connect(self._on_clean_keep_manifests_clicked) self.locale_edit = IndicatorLineEdit( f"{self.core.language_code}-{self.core.country_code}", @@ -101,32 +103,22 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.locale_layout.addWidget(self.locale_edit) self.ui.fetch_win32_check.setChecked(self.settings.get_value(app_settings.win32_meta)) - self.ui.fetch_win32_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.win32_meta, s != Qt.CheckState.Unchecked) - ) + self.ui.fetch_win32_check.checkStateChanged.connect(self._on_fetch_win32_changed) self.ui.fetch_macos_check.setChecked(self.settings.get_value(app_settings.macos_meta)) - self.ui.fetch_macos_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.macos_meta, s != Qt.CheckState.Unchecked) - ) + self.ui.fetch_macos_check.checkStateChanged.connect(self._on_fetch_macos_changed) self.ui.fetch_macos_check.setDisabled(pf.system() == "Darwin") self.ui.fetch_unreal_check.setChecked(self.settings.get_value(app_settings.unreal_meta)) - self.ui.fetch_unreal_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.unreal_meta, s != Qt.CheckState.Unchecked) - ) + self.ui.fetch_unreal_check.checkStateChanged.connect(self._on_fetch_unreal_changed) self.ui.exclude_non_asset_check.setChecked(self.settings.get_value(app_settings.exclude_non_asset)) - self.ui.exclude_non_asset_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.exclude_non_asset, s != Qt.CheckState.Unchecked) - ) + self.ui.exclude_non_asset_check.checkStateChanged.connect(self._on_exclude_non_asset_changed) self.ui.exclude_entitlements_check.setChecked(self.settings.get_value(app_settings.exclude_entitlements)) - self.ui.exclude_entitlements_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.exclude_entitlements, s != Qt.CheckState.Unchecked) - ) + self.ui.exclude_entitlements_check.checkStateChanged.connect(self._on_exclude_entitlements_changed) - self.ui.refresh_metadata_button.clicked.connect(self.refresh_metadata) + self.ui.refresh_metadata_button.clicked.connect(self._refresh_metadata) # FIXME: Disable the button for now because it interferes with RareCore self.ui.refresh_metadata_button.setEnabled(False) self.ui.refresh_metadata_button.setVisible(False) @@ -145,15 +137,19 @@ def hideEvent(self, a0: QHideEvent): self.core.lgd.save_config() return super().hideEvent(a0) - def refresh_metadata(self): + @Slot() + def _on_refresh_worker_finished(self): + self.ui.refresh_metadata_button.setDisabled(False) + + def _refresh_metadata(self): self.ui.refresh_metadata_button.setDisabled(True) platforms = set() if self.ui.fetch_win32_check.isChecked(): platforms.add("Win32") if self.ui.fetch_macos_check.isChecked(): platforms.add("Mac") - worker = RefreshGameMetaWorker(platforms, self.ui.fetch_unreal_check.isChecked()) - worker.signals.finished.connect(lambda: self.ui.refresh_metadata_button.setDisabled(False)) + worker = RefreshGameMetaWorker(self.core, platforms, self.ui.fetch_unreal_check.isChecked()) + worker.signals.finished.connect(self._on_refresh_worker_finished) QThreadPool.globalInstance().start(worker) @staticmethod @@ -191,16 +187,16 @@ def __path_edit_callback(path: str) -> Tuple[bool, str, int]: return True, path, IndicatorReasonsCommon.VALID @Slot(str) - def __path_save_callback_mac(self, text: str) -> None: - self.__path_save(text, "mac_install_dir") + def _path_save_callback_mac(self, text: str) -> None: + self._path_save(text, "mac_install_dir") @Slot(str) - def __path_save_callback_win(self, text: str) -> None: - self.__path_save(text, "install_dir") + def _path_save_callback_win(self, text: str) -> None: + self._path_save(text, "install_dir") if pf.system() != "Darwin": - self.__path_save_callback_mac(text) + self._path_save_callback_mac(text) - def __path_save(self, text: str, option: str): + def _path_save(self, text: str, option: str): if text: self.core.lgd.config.set("Legendary", option, text) else: @@ -256,3 +252,31 @@ def clean_metadata(self, keep_manifests: bool): ) else: QMessageBox.information(self, self.tr("Cleanup"), self.tr("Nothing to clean")) + + @Slot() + def _on_clean_clicked(self): + self.clean_metadata(keep_manifests=False) + + @Slot() + def _on_clean_keep_manifests_clicked(self): + self.clean_metadata(keep_manifests=True) + + @Slot(Qt.CheckState) + def _on_fetch_win32_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.win32_meta, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_fetch_macos_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.macos_meta, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_fetch_unreal_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.unreal_meta, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_exclude_non_asset_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.exclude_non_asset, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_exclude_entitlements_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.exclude_entitlements, state != Qt.CheckState.Unchecked) diff --git a/rare/components/tabs/settings/rare.py b/rare/components/tabs/settings/rare.py index a9c278ac14..c5923c998f 100644 --- a/rare/components/tabs/settings/rare.py +++ b/rare/components/tabs/settings/rare.py @@ -47,7 +47,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.lang_select.setCurrentIndex(index) else: self.ui.lang_select.setCurrentIndex(0) - self.ui.lang_select.currentIndexChanged.connect(self.on_lang_changed) + self.ui.lang_select.currentIndexChanged.connect(self._on_lang_changed) self.ui.color_select.addItem(self.tr("None"), "") for item in get_color_schemes(): @@ -59,7 +59,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.style_select.setDisabled(True) else: self.ui.color_select.setCurrentIndex(0) - self.ui.color_select.currentIndexChanged.connect(self.on_color_select_changed) + self.ui.color_select.currentIndexChanged.connect(self._on_color_select_changed) self.ui.style_select.addItem(self.tr("None"), "") for item in get_style_sheets(): @@ -71,7 +71,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.color_select.setDisabled(True) else: self.ui.style_select.setCurrentIndex(0) - self.ui.style_select.currentIndexChanged.connect(self.on_style_select_changed) + self.ui.style_select.currentIndexChanged.connect(self._on_style_select_changed) self.ui.view_combo.addItem(self.tr("Game covers"), LibraryView.COVER) self.ui.view_combo.addItem(self.tr("Vertical list"), LibraryView.VLIST) @@ -80,56 +80,36 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.view_combo.setCurrentIndex(index) else: self.ui.view_combo.setCurrentIndex(0) - self.ui.view_combo.currentIndexChanged.connect(self.on_view_combo_changed) + self.ui.view_combo.currentIndexChanged.connect(self._on_view_combo_changed) self.discord_rpc_settings = DiscordRPCSettings(settings, rcore.signals(), self) self.ui.right_layout.insertWidget(1, self.discord_rpc_settings, alignment=Qt.AlignmentFlag.AlignTop) self.ui.sys_tray_close.setChecked(self.settings.get_value(app_settings.sys_tray_close)) - self.ui.sys_tray_close.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.sys_tray_close, s != Qt.CheckState.Unchecked) - ) + self.ui.sys_tray_close.checkStateChanged.connect(self._on_sys_tray_close_changed) self.ui.sys_tray_start.setChecked(self.settings.get_value(app_settings.sys_tray_start)) - self.ui.sys_tray_start.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.sys_tray_start, s != Qt.CheckState.Unchecked) - ) - - # Disable starting in system tray if closing to system tray is disabled. - self.ui.sys_tray_close.checkStateChanged.connect(lambda: self.ui.sys_tray_start.setChecked(False)) - self.ui.sys_tray_close.checkStateChanged.connect( - lambda s: self.ui.sys_tray_start.setEnabled(s != Qt.CheckState.Unchecked) - ) + self.ui.sys_tray_start.checkStateChanged.connect(self._on_sys_tray_start_changed) self.ui.auto_update.setChecked(self.settings.get_value(app_settings.auto_update)) - self.ui.auto_update.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.auto_update, s != Qt.CheckState.Unchecked) - ) + self.ui.auto_update.checkStateChanged.connect(self._on_auto_update_changed) self.ui.confirm_start.setChecked(self.settings.get_value(app_settings.confirm_start)) - self.ui.confirm_start.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.confirm_start, s != Qt.CheckState.Unchecked) - ) + self.ui.confirm_start.checkStateChanged.connect(self._on_confirm_start_changed) # TODO: implement use when starting game, disable for now self.ui.confirm_start.setDisabled(True) self.ui.auto_sync_cloud.setChecked(self.settings.get_value(app_settings.auto_sync_cloud)) - self.ui.auto_sync_cloud.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.auto_sync_cloud, s != Qt.CheckState.Unchecked) - ) + self.ui.auto_sync_cloud.checkStateChanged.connect(self._on_auto_sync_cloud_changed) self.ui.notification.setChecked(self.settings.get_value(app_settings.notification)) - self.ui.notification.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.notification, s != Qt.CheckState.Unchecked) - ) + self.ui.notification.checkStateChanged.connect(self._on_notification_changed) self.ui.save_size.setChecked(self.settings.get_value(app_settings.restore_window)) - self.ui.save_size.checkStateChanged.connect(self.save_window_size) + self.ui.save_size.checkStateChanged.connect(self._on_save_size_changed) self.ui.log_games.setChecked(self.settings.get_value(app_settings.log_games)) - self.ui.log_games.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.log_games, s != Qt.CheckState.Unchecked) - ) + self.ui.log_games.checkStateChanged.connect(self._on_log_games_changed) if desktop_links_supported(): self.desktop_link = desktop_link_path("Rare", "desktop") @@ -148,20 +128,48 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): if self.start_menu_link and self.start_menu_link.exists(): self.ui.startmenu_link_btn.setText(self.tr("Remove start menu link")) - self.ui.desktop_link_btn.clicked.connect(self.create_desktop_link) - self.ui.startmenu_link_btn.clicked.connect(self.create_start_menu_link) + self.ui.desktop_link_btn.clicked.connect(self._create_desktop_link) + self.ui.startmenu_link_btn.clicked.connect(self._create_start_menu_link) - self.ui.log_dir_open_button.clicked.connect(self.open_directory) - self.ui.log_dir_clean_button.clicked.connect(self.clean_logdir) + self.ui.log_dir_open_button.clicked.connect(self._open_directory) + self.ui.log_dir_clean_button.clicked.connect(self._clean_logdir) # get size of logdir size = sum(log_dir().joinpath(f).stat().st_size for f in log_dir().iterdir() if log_dir().joinpath(f).is_file()) self.ui.log_dir_size_label.setText(format_size(size)) - # self.log_dir_clean_button.setVisible(False) - # self.log_dir_size_label.setVisible(False) + + @Slot(Qt.CheckState) + def _on_sys_tray_close_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.sys_tray_close, state != Qt.CheckState.Unchecked) + self.ui.sys_tray_start.setChecked(False) + self.ui.sys_tray_start.setEnabled(state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_sys_tray_start_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.sys_tray_start, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_auto_update_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.auto_update, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_confirm_start_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.confirm_start, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_auto_sync_cloud_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.auto_sync_cloud, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_notification_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.notification, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_log_games_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.log_games, state != Qt.CheckState.Unchecked) @Slot() - def clean_logdir(self): + def _clean_logdir(self): for f in log_dir().iterdir(): try: if log_dir().joinpath(f).is_file(): @@ -172,7 +180,7 @@ def clean_logdir(self): self.ui.log_dir_size_label.setText(format_size(size)) @Slot() - def create_start_menu_link(self): + def _create_start_menu_link(self): try: if not os.path.exists(self.start_menu_link): if not create_desktop_link(app_name="rare_shortcut", link_type="start_menu"): @@ -190,7 +198,7 @@ def create_start_menu_link(self): ) @Slot() - def create_desktop_link(self): + def _create_desktop_link(self): try: if not os.path.exists(self.desktop_link): if not create_desktop_link(app_name="rare_shortcut", link_type="desktop"): @@ -208,7 +216,7 @@ def create_desktop_link(self): ) @Slot(int) - def on_color_select_changed(self, index: int): + def _on_color_select_changed(self, index: int): scheme = self.ui.color_select.itemData(index, Qt.ItemDataRole.UserRole) if scheme: self.ui.style_select.setCurrentIndex(0) @@ -219,7 +227,7 @@ def on_color_select_changed(self, index: int): set_color_pallete(scheme) @Slot(int) - def on_style_select_changed(self, index: int): + def _on_style_select_changed(self, index: int): style = self.ui.style_select.itemData(index, Qt.ItemDataRole.UserRole) if style: self.ui.color_select.setCurrentIndex(0) @@ -230,22 +238,22 @@ def on_style_select_changed(self, index: int): set_style_sheet(style) @Slot(int) - def on_view_combo_changed(self, index: int): + def _on_view_combo_changed(self, index: int): view = LibraryView(self.ui.view_combo.itemData(index, Qt.ItemDataRole.UserRole)) self.settings.set_value(app_settings.library_view, view) @Slot() - def open_directory(self): + def _open_directory(self): QDesktopServices.openUrl(QUrl.fromLocalFile(log_dir())) @Slot(Qt.CheckState) - def save_window_size(self, state: Qt.CheckState): + def _on_save_size_changed(self, state: Qt.CheckState): self.settings.set_value(app_settings.restore_window, state != Qt.CheckState.Unchecked) self.settings.rem_value(app_settings.window_width) self.settings.rem_value(app_settings.window_height) @Slot(int) - def on_lang_changed(self, index: int): + def _on_lang_changed(self, index: int): lang_code = self.ui.lang_select.itemData(index, Qt.ItemDataRole.UserRole) if lang_code == locale.getlocale()[0]: self.settings.rem_value(app_settings.language) diff --git a/rare/components/tabs/settings/widgets/discord_rpc.py b/rare/components/tabs/settings/widgets/discord_rpc.py index e9406a2e44..088a19c183 100644 --- a/rare/components/tabs/settings/widgets/discord_rpc.py +++ b/rare/components/tabs/settings/widgets/discord_rpc.py @@ -1,6 +1,6 @@ import importlib.util -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Slot from PySide6.QtWidgets import QGroupBox from rare.models.settings import DiscordRPCMode, RareAppSettings, app_settings @@ -28,28 +28,35 @@ def __init__(self, settings: RareAppSettings, signals: GlobalSignals, parent): self.ui.mode_combo.setCurrentIndex(self.ui.mode_combo.findData(rpc_mode, Qt.ItemDataRole.UserRole)) else: self.ui.mode_combo.setCurrentIndex(index) - self.ui.mode_combo.currentIndexChanged.connect(self.__mode_changed) + self.ui.mode_combo.currentIndexChanged.connect(self._mode_changed) self.ui.game_check.setChecked(self.settings.get_value(app_settings.discord_rpc_game)) - self.ui.game_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.discord_rpc_game, s != Qt.CheckState.Unchecked) - ) + self.ui.game_check.checkStateChanged.connect(self._on_game_changed) self.ui.os_check.setChecked(self.settings.get_value(app_settings.discord_rpc_os)) - self.ui.os_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.discord_rpc_os, s != Qt.CheckState.Unchecked) - ) + self.ui.os_check.checkStateChanged.connect(self._on_os_changed) self.ui.time_check.setChecked(self.settings.get_value(app_settings.discord_rpc_time)) - self.ui.time_check.checkStateChanged.connect( - lambda s: self.settings.set_value(app_settings.discord_rpc_time, s != Qt.CheckState.Unchecked) - ) + self.ui.time_check.checkStateChanged.connect(self._on_time_changed) if not importlib.util.find_spec("pypresence"): self.setDisabled(True) self.setToolTip(self.tr("Pypresence is not installed")) - def __mode_changed(self, index): + @Slot(Qt.CheckState) + def _on_game_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.discord_rpc_game, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_os_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.discord_rpc_os, state != Qt.CheckState.Unchecked) + + @Slot(Qt.CheckState) + def _on_time_changed(self, state: Qt.CheckState): + self.settings.set_value(app_settings.discord_rpc_time, state != Qt.CheckState.Unchecked) + + @Slot(int) + def _mode_changed(self, index: int): data = self.ui.mode_combo.itemData(index, Qt.ItemDataRole.UserRole) self.settings.set_value(app_settings.discord_rpc_mode, data) self.signals.discord_rpc.update_settings.emit() diff --git a/rare/components/tabs/settings/widgets/wrappers.py b/rare/components/tabs/settings/widgets/wrappers.py index d2f9e2086c..cfa7b58838 100644 --- a/rare/components/tabs/settings/widgets/wrappers.py +++ b/rare/components/tabs/settings/widgets/wrappers.py @@ -164,11 +164,13 @@ def data(self) -> Wrapper: def __on_state_changed(self, state: Qt.CheckState) -> None: new_wrapper = Wrapper(command=self.wrapper.command, enabled=self.text_lbl.isChecked()) self.update_wrapper.emit(self.wrapper, new_wrapper) + self.disconnect(self) self.deleteLater() @Slot() def __on_delete(self) -> None: self.delete_wrapper.emit(self.wrapper) + self.disconnect(self) self.deleteLater() @Slot() @@ -183,6 +185,7 @@ def __on_edit_result(self, accepted: bool, command: str): if accepted and command: new_wrapper = Wrapper(command=shlex.split(command)) self.update_wrapper.emit(self.wrapper, new_wrapper) + self.disconnect(self) self.deleteLater() def mouseMoveEvent(self, a0: QMouseEvent) -> None: @@ -381,6 +384,7 @@ def __update_wrapper(self, old: Wrapper, new: Wrapper): @Slot() def update_state(self): for w in self.wrapper_container.findChildren(WrapperWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): + w.disconnect(w) w.deleteLater() wrappers = self.wrappers.get_wrappers(self.app_name) if not wrappers: diff --git a/rare/components/tabs/store/landing.py b/rare/components/tabs/store/landing.py index 7072b496b8..820c54d42f 100644 --- a/rare/components/tabs/store/landing.py +++ b/rare/components/tabs/store/landing.py @@ -102,8 +102,8 @@ def __init__(self, api: StoreAPI, parent=None): def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - self.api.get_free(self.__update_free_games) - self.api.get_wishlist(self.__update_wishlist_discounts) + self.api.get_free(self._update_free_games) + self.api.get_wishlist(self._update_wishlist_discounts) return super().showEvent(a0) def hideEvent(self, a0: QHideEvent) -> None: @@ -112,9 +112,10 @@ def hideEvent(self, a0: QHideEvent) -> None: # TODO: Implement tab unloading return super().hideEvent(a0) - def __update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): + def _update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): for w in self.discounts_group.findChildren(StoreItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.discounts_group.layout().removeWidget(w) + w.disconnect(w) w.deleteLater() for item in filter(lambda x: bool(x.offer.price.totalPrice.discount), wishlist): @@ -125,13 +126,15 @@ def __update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): self.discounts_group.setVisible(have_discounts) self.discounts_group.loading(False) - def __update_free_games(self, free_games: List[CatalogOfferModel]): + def _update_free_games(self, free_games: List[CatalogOfferModel]): for w in self.free_games_now.findChildren(StoreItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.free_games_now.layout().removeWidget(w) + w.disconnect(w) w.deleteLater() for w in self.free_games_next.findChildren(StoreItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.free_games_next.layout().removeWidget(w) + w.disconnect(w) w.deleteLater() date = datetime.now(timezone.utc) @@ -182,6 +185,7 @@ def show_games(self, data): for w in self.games_group.findChildren(StoreItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.games_group.layout().removeWidget(w) + w.disconnect(w) w.deleteLater() for game in data: diff --git a/rare/components/tabs/store/search.py b/rare/components/tabs/store/search.py index cf6522ac98..4284b02fd0 100644 --- a/rare/components/tabs/store/search.py +++ b/rare/components/tabs/store/search.py @@ -101,16 +101,37 @@ def show_search_results(self): # self.show_info.emit(self.search_bar.text()) def init_filter(self): - self.ui.none_price.toggled.connect(lambda: self.prepare_request("") if self.ui.none_price.isChecked() else None) - self.ui.free_button.toggled.connect(lambda: self.prepare_request("free") if self.ui.free_button.isChecked() else None) - self.ui.under10.toggled.connect(lambda: self.prepare_request("[0, 1000)") if self.ui.under10.isChecked() else None) - self.ui.under20.toggled.connect(lambda: self.prepare_request("[0, 2000)") if self.ui.under20.isChecked() else None) - self.ui.under30.toggled.connect(lambda: self.prepare_request("[0, 3000)") if self.ui.under30.isChecked() else None) - self.ui.above.toggled.connect(lambda: self.prepare_request("[1499,]") if self.ui.above.isChecked() else None) + # self.ui.none_price.toggled.connect(lambda: self.prepare_request("") if self.ui.none_price.isChecked() else None) + self.ui.none_price.toggled.connect( + (lambda obj: obj.prepare_request("") if self.ui.none_price.isChecked() else None).__get__(self) + ) + # self.ui.free_button.toggled.connect(lambda: self.prepare_request("free") if self.ui.free_button.isChecked() else None) + self.ui.free_button.toggled.connect( + (lambda obj: obj.prepare_request("free") if self.ui.free_button.isChecked() else None).__get__(self) + ) + # self.ui.under10.toggled.connect(lambda: self.prepare_request("[0, 1000)") if self.ui.under10.isChecked() else None) + self.ui.under10.toggled.connect( + (lambda obj: obj.prepare_request("[0, 1000)") if self.ui.under10.isChecked() else None).__get__(self) + ) + # self.ui.under20.toggled.connect(lambda: self.prepare_request("[0, 2000)") if self.ui.under20.isChecked() else None) + self.ui.under20.toggled.connect( + (lambda obj: obj.prepare_request("[0, 2000)") if self.ui.under20.isChecked() else None).__get__(self) + ) + # self.ui.under30.toggled.connect(lambda: self.prepare_request("[0, 3000)") if self.ui.under30.isChecked() else None) + self.ui.under30.toggled.connect( + (lambda obj: obj.prepare_request("[0, 3000)") if self.ui.under30.isChecked() else None).__get__(self) + ) + # self.ui.above.toggled.connect(lambda: self.prepare_request("[1499,]") if self.ui.above.isChecked() else None) + self.ui.above.toggled.connect( + (lambda obj: obj.prepare_request("[1499,]") if self.ui.above.isChecked() else None).__get__(self) + ) # self.on_discount.toggled.connect( # lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None # ) - self.ui.on_discount.toggled.connect(lambda: self.prepare_request()) + # self.ui.on_discount.toggled.connect(lambda: self.prepare_request()) + self.ui.on_discount.toggled.connect( + (lambda obj: obj.prepare_request()).__get__(self) + ) constants = Constants() self.checkboxes = [] @@ -123,8 +144,14 @@ def init_filter(self): ]: for text, tag in variables: checkbox = CheckBox(text, tag) - checkbox.activated.connect(lambda x: self.prepare_request(added_tag=x)) - checkbox.deactivated.connect(lambda x: self.prepare_request(removed_tag=x)) + # checkbox.activated.connect(lambda x: self.prepare_request(added_tag=x)) + checkbox.activated.connect( + (lambda obj, x: obj.prepare_request(added_tag=x)).__get__(self) + ) + # checkbox.deactivated.connect(lambda x: self.prepare_request(removed_tag=x)) + checkbox.deactivated.connect( + (lambda obj, x: obj.prepare_request(removed_tag=x)).__get__(self) + ) groupbox.layout().addWidget(checkbox) self.checkboxes.append(checkbox) self.ui.reset_button.clicked.connect(self.reset_filters) @@ -237,9 +264,11 @@ def load_results(self, text: str): def show_results(self, results: dict): for w in self.results_container.findChildren(QLabel, options=Qt.FindChildOption.FindDirectChildrenOnly): self.results_layout.removeWidget(w) + w.disconnect(w) w.deleteLater() for w in self.results_container.findChildren(SearchItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.results_layout.removeWidget(w) + w.disconnect(w) w.deleteLater() if not results: @@ -247,7 +276,7 @@ def show_results(self, results: dict): else: for res in results: w = SearchItemWidget(self.store_api.cached_manager, res, parent=self.results_container) - w.show_details.connect(self.show_details.emit) + w.show_details.connect(self.show_details) self.results_layout.addWidget(w) self.results_layout.update() self.setEnabled(True) diff --git a/rare/components/tabs/store/store_api.py b/rare/components/tabs/store/store_api.py index ff8f7da3f7..1d513b74aa 100644 --- a/rare/components/tabs/store/store_api.py +++ b/rare/components/tabs/store/store_api.py @@ -1,4 +1,5 @@ from logging import getLogger +from typing import Callable, Tuple from PySide6.QtCore import QObject, Signal from PySide6.QtWidgets import QApplication @@ -45,7 +46,7 @@ def __init__(self, token, language: str, country: str, installed): self.browse_active = False self.next_browse_request = tuple(()) - def get_free(self, callback: callable): + def get_free(self, callback: Callable): url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions" params = { "locale": self.locale, @@ -54,7 +55,7 @@ def get_free(self, callback: callable): } self.manager.get(url, lambda data: self.__handle_free_games(data, callback), params=params) - def __handle_free_games(self, data, callback): + def __handle_free_games(self, data, callback: Callable): try: response = ResponseModel.from_dict(data) if response.errors: @@ -68,7 +69,7 @@ def __handle_free_games(self, data, callback): self.logger.error("Free games request failed with: %s", e) callback(elements) - def get_wishlist(self, callback): + def get_wishlist(self, callback: Callable): self.authed_manager.post( graphql_url, lambda data: self.__handle_wishlist(data, callback), @@ -82,7 +83,7 @@ def get_wishlist(self, callback): }, ) - def __handle_wishlist(self, data, callback): + def __handle_wishlist(self, data, callback: Callable[[Tuple], None]): try: response = ResponseModel.from_dict(data) if response.errors: @@ -96,7 +97,7 @@ def __handle_wishlist(self, data, callback): self.logger.error("Wishlist request failed with: %s", e) callback(elements) - def search_game(self, name, callback): + def search_game(self, name, callback: Callable): payload = { "query": search_query, "variables": { @@ -116,7 +117,7 @@ def search_game(self, name, callback): self.manager.post(graphql_url, lambda data: self.__handle_search(data, callback), payload) - def __handle_search(self, data, callback): + def __handle_search(self, data, callback: Callable[[Tuple], None]): try: response = ResponseModel.from_dict(data) if response.errors: @@ -177,7 +178,7 @@ def __make_graphql_query(self): def __make_api_query(self): pass - def get_game_config_cms(self, slug: str, is_bundle: bool, callback): + def get_game_config_cms(self, slug: str, is_bundle: bool, callback: Callable): url = "https://store-content.ak.epicgames.com/api" url += f"/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" self.logger.debug("Quering game config: %s", url) @@ -194,7 +195,7 @@ def __handle_get_game(self, data, callback): # callback({}) # needs a captcha - def add_to_wishlist(self, namespace, offer_id, callback: callable): + def add_to_wishlist(self, namespace, offer_id, callback: Callable): payload = { "query": wishlist_add_query, "variables": { @@ -225,7 +226,7 @@ def _handle_add_to_wishlist(self, data, callback): callback(success) self.update_wishlist.emit() - def remove_from_wishlist(self, namespace, offer_id, callback: callable): + def remove_from_wishlist(self, namespace, offer_id, callback: Callable): payload = { "query": wishlist_remove_query, "variables": { diff --git a/rare/components/tabs/store/widgets/details.py b/rare/components/tabs/store/widgets/details.py index 1ad4100696..6221055775 100644 --- a/rare/components/tabs/store/widgets/details.py +++ b/rare/components/tabs/store/widgets/details.py @@ -1,7 +1,7 @@ from logging import getLogger from typing import List -from PySide6.QtCore import Qt, QUrl, Signal +from PySide6.QtCore import Qt, QUrl, Signal, Slot from PySide6.QtGui import QDesktopServices, QKeyEvent from PySide6.QtWidgets import ( QGridLayout, @@ -83,6 +83,7 @@ def update_game(self, offer: CatalogOfferModel): # lk: delete tabs in reverse order because indices are updated on deletion while self.requirements_tabs.count(): + self.requirements_tabs.widget(0).disconnect(self.requirements_tabs.widget(0)) self.requirements_tabs.widget(0).deleteLater() self.requirements_tabs.removeTab(0) self.requirements_tabs.clear() @@ -202,6 +203,7 @@ def data_received(self, product: DieselProduct): # clear Layout for b in self.ui.social_links.findChildren(SocialButton, options=Qt.FindChildOption.FindDirectChildrenOnly): self.ui.social_links_layout.removeWidget(b) + b.disconnect(b) b.deleteLater() links = product_data.socialLinks @@ -248,9 +250,13 @@ def __init__(self, icn, url, parent=None): self.setFixedSize(36, 36) self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.url = url - self.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url))) + self.clicked.connect(self._on_clicked) self.setToolTip(url) + @Slot() + def _on_clicked(self): + QDesktopServices.openUrl(QUrl(self.url)) + class RequirementsWidget(QWidget, SideTabContents): def __init__(self, system: DieselSystemDetail, parent=None): diff --git a/rare/components/tabs/store/widgets/items.py b/rare/components/tabs/store/widgets/items.py index 3e2528df9c..f496f8d755 100644 --- a/rare/components/tabs/store/widgets/items.py +++ b/rare/components/tabs/store/widgets/items.py @@ -1,6 +1,6 @@ from logging import getLogger -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QMouseEvent from PySide6.QtWidgets import QPushButton @@ -106,6 +106,7 @@ def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent= super(WishlistItemWidget, self).__init__(manager, catalog_game, parent=parent) self.setFixedSize(ImageSize.DisplayWide) self.ui.setupUi(self) + for attr in catalog_game.customAttributes: if attr["key"] == "developerName": developer = attr["value"] @@ -130,5 +131,9 @@ def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent= self.delete_button = QPushButton(self) self.delete_button.setIcon(qta_icon("mdi.delete", color="white")) - self.delete_button.clicked.connect(lambda: self.delete_from_wishlist.emit(self.catalog_game)) + self.delete_button.clicked.connect(self._on_delete_clicked) self.layout().insertWidget(0, self.delete_button, alignment=Qt.AlignmentFlag.AlignRight) + + @Slot() + def _on_delete_clicked(self): + self.delete_from_wishlist.emit(self.catalog_game) diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index e250a76072..43d3a64205 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -92,17 +92,17 @@ def __init__(self, api: StoreAPI, parent=None): self.ui.order_combo.currentIndexChanged.connect(self.order_wishlist) self.ui.reload_button.setIcon(qta_icon("fa.refresh", "fa5s.sync", color="white")) - self.ui.reload_button.clicked.connect(self.__update_widget) + self.ui.reload_button.clicked.connect(self._update_widget) - self.ui.reverse_check.stateChanged.connect(lambda: self.order_wishlist(self.ui.order_combo.currentIndex())) + self.ui.reverse_check.stateChanged.connect(self._on_reverse_changed) self.setEnabled(False) def showEvent(self, a0: QShowEvent) -> None: - self.__update_widget() + self._update_widget() return super().showEvent(a0) - def __update_widget(self): + def _update_widget(self): self.setEnabled(False) self.api.get_wishlist(self.set_wishlist) @@ -110,7 +110,7 @@ def delete_from_wishlist(self, game: CatalogOfferModel): self.api.remove_from_wishlist( game.namespace, game.id, - lambda success: self.__update_widget() + lambda success: self._update_widget() if success else QMessageBox.warning(self, "Error", self.tr("Could not remove game from wishlist")), ) @@ -130,6 +130,13 @@ def filter_wishlist(self, index: int = int(WishlistFilter.NONE)): have_visible = any(map(lambda x: x.isVisible(), widgets)) self.ui.no_games_label.setVisible(not have_visible) + __ordering = { + WishlistOrder.NAME: lambda x: x.catalog_game.title, + WishlistOrder.PRICE: lambda x: x.catalog_game.price.totalPrice.discountPrice, + WishlistOrder.DEVELOPER: lambda x: x.catalog_game.seller["name"], + WishlistOrder.DISCOUNT: lambda x: 1 - (x.catalog_game.price.totalPrice.discountPrice / x.catalog_game.price.totalPrice.originalPrice) + } + @Slot(int) def order_wishlist(self, index: int = int(WishlistOrder.NAME)): list_order = self.ui.order_combo.itemData(index, Qt.ItemDataRole.UserRole) @@ -137,34 +144,16 @@ def order_wishlist(self, index: int = int(WishlistOrder.NAME)): for w in widgets: self.wishlist_layout.removeWidget(w) - if list_order == WishlistOrder.NAME: - - def func(x: WishlistItemWidget): - return x.catalog_game.title - elif list_order == WishlistOrder.PRICE: - - def func(x: WishlistItemWidget): - return x.catalog_game.price.totalPrice.discountPrice - elif list_order == WishlistOrder.DEVELOPER: - - def func(x: WishlistItemWidget): - return x.catalog_game.seller["name"] - elif list_order == WishlistOrder.DISCOUNT: - - def func(x: WishlistItemWidget): - discount = x.catalog_game.price.totalPrice.discountPrice - original = x.catalog_game.price.totalPrice.originalPrice - return 1 - (discount / original) - else: - - def func(x: WishlistItemWidget): - return x.catalog_game.title - reverse = self.ui.reverse_check.isChecked() - widgets = sorted(widgets, key=func, reverse=reverse) + widgets = sorted(widgets, key=self.__ordering[list_order], reverse=reverse) for w in widgets: self.wishlist_layout.addWidget(w) + @Slot(Qt.CheckState) + def _on_reverse_changed(self, state: Qt.CheckState): + self.order_wishlist(self.ui.order_combo.currentIndex()) + + @Slot(object) def set_wishlist(self, wishlist: List[WishlistItemModel] = None): if wishlist and wishlist[0] == "error": return @@ -172,17 +161,24 @@ def set_wishlist(self, wishlist: List[WishlistItemModel] = None): widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly) for w in widgets: self.wishlist_layout.removeWidget(w) + w.disconnect(w) w.deleteLater() self.ui.no_games_label.setVisible(bool(wishlist)) + widgets = [] for game in wishlist: w = WishlistItemWidget(self.api.cached_manager, game.offer, self.ui.container) w.show_details.connect(self.show_details) w.delete_from_wishlist.connect(self.delete_from_wishlist) + widgets.append(w) + + list_order = self.ui.order_combo.currentData(Qt.ItemDataRole.UserRole) + reverse = self.ui.reverse_check.isChecked() + widgets = sorted(widgets, key=self.__ordering[list_order], reverse=reverse) + for w in widgets: self.wishlist_layout.addWidget(w) - self.order_wishlist(self.ui.order_combo.currentIndex()) self.filter_wishlist(self.ui.filter_combo.currentIndex()) self.setEnabled(True) diff --git a/rare/components/tray_icon.py b/rare/components/tray_icon.py index 15f42457ea..a17dfe851a 100644 --- a/rare/components/tray_icon.py +++ b/rare/components/tray_icon.py @@ -43,7 +43,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): # We need to reference this separator to add game actions before it self.separator = self.menu.addSeparator() self.exit_action = QAction(self.tr("Quit")) - self.exit_action.triggered.connect(lambda: self.exit_app.emit(0)) + self.exit_action.triggered.connect(self._on_exit_triggered) self.menu.addAction(self.exit_action) self.game_actions: List[QAction] = [] @@ -60,6 +60,10 @@ def last_played(self) -> List: last_played.sort(key=lambda g: g.metadata.last_played, reverse=True) return last_played[:5] + @Slot() + def _on_exit_triggered(self): + self.exit_app.emit(0) + @Slot(str, str) def notify(self, title: str, body: str): if self.settings.get_value(app_settings.notification): diff --git a/rare/models/game.py b/rare/models/game.py index 55dd842eb7..06012d7266 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -14,7 +14,7 @@ from PySide6.QtGui import QPixmap from rare.lgndr.core import LegendaryCore -from rare.models.base_game import RareGameBase, RareGameSlim +from rare.models.game_slim import RareGameBase, RareGameSlim from rare.models.image import ImageSize from rare.models.install import InstallOptionsModel, UninstallOptionsModel from rare.models.settings import RareAppSettings, app_settings @@ -26,58 +26,61 @@ from rare.utils.workarounds import apply_workarounds -class RareGame(RareGameSlim): - @dataclass - class Metadata: - queued: bool = False - queue_pos: Optional[int] = None - last_played: datetime = datetime.min.replace(tzinfo=timezone.utc) - achievements_date: datetime = datetime.min.replace(tzinfo=timezone.utc) - grant_date: datetime = datetime.min.replace(tzinfo=timezone.utc) - steam_appid: Optional[str] = None - steam_grade: Optional[str] = None - steam_date: datetime = datetime.min.replace(tzinfo=timezone.utc) - steam_shortcut: Optional[int] = None - tags: Tuple[str, ...] = field(default_factory=tuple) - - # For compatibility with previously created game metadata - @staticmethod - def parse_date(strdate: str): - dt = datetime.fromisoformat(strdate) if strdate else datetime.min - return dt.replace(tzinfo=timezone.utc) - - @classmethod - def from_dict(cls, data: Dict): - return cls( - queued=data.get("queued", False), - queue_pos=data.get("queue_pos", None), - last_played=RareGame.Metadata.parse_date(data.get("last_played", "")), - achievements_date=RareGame.Metadata.parse_date(data.get("achievements_date", "")), - grant_date=RareGame.Metadata.parse_date(data.get("grant_date", "")), - steam_appid=str(appid) if (appid := data.get("steam_appid", "")) else None, - steam_grade=data.get("steam_grade", None), - steam_date=RareGame.Metadata.parse_date(data.get("steam_date", "")), - steam_shortcut=data.get("steam_shortcut", None), - tags=data.get("tags", ()), - ) +@dataclass +class RareGameMetadata: + queued: bool = False + queue_pos: Optional[int] = None + last_played: datetime = datetime.min.replace(tzinfo=timezone.utc) + achievements_date: datetime = datetime.min.replace(tzinfo=timezone.utc) + grant_date: datetime = datetime.min.replace(tzinfo=timezone.utc) + steam_appid: Optional[str] = None + steam_grade: Optional[str] = None + steam_date: datetime = datetime.min.replace(tzinfo=timezone.utc) + steam_shortcut: Optional[int] = None + tags: Tuple[str, ...] = field(default_factory=tuple) + + # For compatibility with previously created game metadata + @staticmethod + def parse_date(strdate: str): + dt = datetime.fromisoformat(strdate) if strdate else datetime.min + return dt.replace(tzinfo=timezone.utc) + + @classmethod + def from_dict(cls, data: Dict): + return cls( + queued=data.get("queued", False), + queue_pos=data.get("queue_pos", None), + last_played=RareGameMetadata.parse_date(data.get("last_played", "")), + achievements_date=RareGameMetadata.parse_date(data.get("achievements_date", "")), + grant_date=RareGameMetadata.parse_date(data.get("grant_date", "")), + steam_appid=str(appid) if (appid := data.get("steam_appid", "")) else None, + steam_grade=data.get("steam_grade", None), + steam_date=RareGameMetadata.parse_date(data.get("steam_date", "")), + steam_shortcut=data.get("steam_shortcut", None), + tags=data.get("tags", ()), + ) - @property - def __dict__(self): - return dict( - queued=self.queued, - queue_pos=self.queue_pos, - last_played=self.last_played.isoformat() if self.last_played else datetime.min.replace(tzinfo=timezone.utc), - achievements_date=self.last_played.isoformat() if self.achievements_date else datetime.min.replace(tzinfo=timezone.utc), - grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min.replace(tzinfo=timezone.utc), - steam_appid=str(self.steam_appid) if self.steam_appid else None, - steam_grade=self.steam_grade, - steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min.replace(tzinfo=timezone.utc), - steam_shortcut=self.steam_shortcut, - tags=self.tags, - ) + @property + def __dict__(self): + return dict( + queued=self.queued, + queue_pos=self.queue_pos, + last_played=self.last_played.isoformat() if self.last_played else datetime.min.replace(tzinfo=timezone.utc), + achievements_date=self.last_played.isoformat() if self.achievements_date else datetime.min.replace( + tzinfo=timezone.utc), + grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min.replace(tzinfo=timezone.utc), + steam_appid=str(self.steam_appid) if self.steam_appid else None, + steam_grade=self.steam_grade, + steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min.replace(tzinfo=timezone.utc), + steam_shortcut=self.steam_shortcut, + tags=self.tags, + ) - def __bool__(self): - return self.queued or self.queue_pos is not None or self.last_played is not None + def __bool__(self): + return self.queued or self.queue_pos is not None or self.last_played is not None + + +class RareGame(RareGameSlim): def __init__( self, @@ -97,7 +100,7 @@ def __init__( self.game.app_title += f" {self.game.app_name.split('_')[-1]}" self.has_pixmap: bool = False - self.metadata: RareGame.Metadata = RareGame.Metadata() + self.metadata: RareGameMetadata = RareGameMetadata() self.__load_metadata() self.grant_date() @@ -109,8 +112,8 @@ def __init__( self.__worker: Optional[QRunnable] = None self.progress: int = 0 - self.signals.progress.start.connect(lambda: self.__on_progress_update(0)) - self.signals.progress.update.connect(self.__on_progress_update) + self.signals.progress.start.connect(self.__on_progress_update) + self.signals.progress.refresh.connect(self.__on_progress_update) self.__steam_grade_pending: bool = False self.game_process = GameProcess(self.game) @@ -125,7 +128,7 @@ def __init__( def add_dlc(self, dlc) -> None: # lk: plug dlc progress signals to the game's dlc.signals.progress.start.connect(self.signals.progress.start) - dlc.signals.progress.update.connect(self.signals.progress.update) + dlc.signals.progress.refresh.connect(self.signals.progress.refresh) dlc.signals.progress.finish.connect(self.signals.progress.finish) dlc.parent_rgame = self self.owned_dlcs.add(dlc) @@ -139,19 +142,27 @@ def parent_rgame(self, rgame: "RareGame") -> None: if self.is_dlc: self.__parent_rgame = rgame - def __on_progress_update(self, progress: int): + @Slot() + @Slot(int) + def __on_progress_update(self, progress: int = 0): self.progress = progress def get_worker(self) -> Optional[QRunnable]: return self.__worker - def set_worker(self, worker: Optional[QRunnable]): + @Slot(object) + def set_worker(self, worker: QRunnable): if worker and self.__worker is not None: - self.logger.error("Game '%s' already has attached worker %s", self.app_title, self.__worker) + self.logger.error("Game '%s' already has attached worker '%s'", self.app_title, str(self.__worker)) raise RuntimeError + worker.feedback.finished.connect(self.del_worker) self.__worker = worker - if worker is None: - self.state = RareGame.State.IDLE + + @Slot(object) + def del_worker(self, worker: QRunnable): + self.logger.debug("Removing worker '%s' from '%s'", str(worker), self.app_title) + self.__worker = None + self.state = RareGame.State.IDLE @Slot(int) def __game_launched(self, code: int): @@ -195,7 +206,7 @@ def __load_metadata(self): # pylint: disable=unsupported-membership-test if self.app_name in metadata: # pylint: disable=unsubscriptable-object - self.metadata = RareGame.Metadata.from_dict(metadata[self.app_name]) + self.metadata = RareGameMetadata.from_dict(metadata[self.app_name]) def __save_metadata(self): with RareGame.__metadata_lock: @@ -467,7 +478,7 @@ def save_path(self, path: str) -> None: if self.igame and (self.game.supports_cloud_saves or self.game.supports_mac_cloud_saves): self.igame.save_path = path self.store_igame() - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() @property def achievements(self) -> Optional[Namespace]: @@ -515,7 +526,7 @@ def eulas(self) -> List: def reset_steam_date(self): self.metadata.steam_date = datetime.min.replace(tzinfo=timezone.utc) - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() @property def steam_appid(self) -> Optional[str]: @@ -552,7 +563,7 @@ def set_steam_grade(self) -> None: self.metadata.steam_date = datetime.now(timezone.utc) self.__steam_grade_pending = False self.__save_metadata() - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() def grant_date(self, force=False) -> datetime: if not (entitlements := self.core.lgd.entitlements): @@ -606,7 +617,7 @@ def get_pixmap(self, preset: ImageSize.Preset, color=True) -> QPixmap: def __update_pixmap(self): self.has_pixmap = self.image_manager.has_pixmaps(self.app_name) if self.has_pixmap: - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() def load_pixmaps(self): """Do not call this function, call set_pixmap instead. This is only used for initial image loading""" @@ -758,7 +769,7 @@ def __init__( def __update_pixmap(self): self.has_pixmap = self.image_manager.has_pixmaps(self.app_name) if self.has_pixmap: - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() @property def is_installed(self) -> bool: @@ -863,3 +874,6 @@ def uninstall(self) -> bool: keep_overlay_keys=platform.system() not in {"Windows"}, )) return True + + +__all__ = ["RareGame", "RareEosOverlay"] \ No newline at end of file diff --git a/rare/models/base_game.py b/rare/models/game_slim.py similarity index 91% rename from rare/models/base_game.py rename to rare/models/game_slim.py index dbce38ea5d..1c4050e96f 100644 --- a/rare/models/base_game.py +++ b/rare/models/game_slim.py @@ -24,41 +24,50 @@ class RareSaveGame: description: Optional[str] = "" +class RareGameSignalsProgress(QObject): + start = Signal() + refresh = Signal(int) + finish = Signal(bool) + + +class RareGameSignalsWidget(QObject): + refresh = Signal() + + +class RareGameSignalsDownload(QObject): + enqueue = Signal(str) + dequeue = Signal(str) + + +class RareGameSignalsGame(QObject): + install = Signal(InstallOptionsModel) + installed = Signal(str) + uninstall = Signal(UninstallOptionsModel) + uninstalled = Signal(str) + launched = Signal(str) + finished = Signal(str) + + class RareGameSignals(QObject): - class Progress(QObject): - start = Signal() - update = Signal(int) - finish = Signal(bool) - - class Widget(QObject): - update = Signal() - - class Download(QObject): - enqueue = Signal(str) - dequeue = Signal(str) - - class Game(QObject): - install = Signal(InstallOptionsModel) - installed = Signal(str) - uninstall = Signal(UninstallOptionsModel) - uninstalled = Signal(str) - launched = Signal(str) - finished = Signal(str) def __init__(self, /): super(RareGameSignals, self).__init__() - self.progress = RareGameSignals.Progress() - self.widget = RareGameSignals.Widget() - self.download = RareGameSignals.Download() - self.game = RareGameSignals.Game() + self.progress = RareGameSignalsProgress() + self.widget = RareGameSignalsWidget() + self.download = RareGameSignalsDownload() + self.game = RareGameSignalsGame() def deleteLater(self): + self.progress.disconnect(self.progress) self.progress.deleteLater() del self.progress + self.widget.disconnect(self.widget) self.widget.deleteLater() del self.widget + self.download.disconnect(self.download) self.download.deleteLater() del self.download + self.game.disconnect(self.game) self.game.deleteLater() del self.game super(RareGameSignals, self).deleteLater() @@ -84,6 +93,7 @@ def __init__(self, legendary_core: LegendaryCore, game: Game): self._state = RareGameBase.State.IDLE def deleteLater(self): + self.signals.disconnect(self.signals) self.signals.deleteLater() del self.signals super(RareGameBase, self).deleteLater() @@ -96,7 +106,7 @@ def state(self) -> "RareGameBase.State": def state(self, state: "RareGameBase.State"): if state != self._state: self._state = state - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() @property def is_idle(self): @@ -324,7 +334,7 @@ def load_saves(self, saves: List[SaveGameFile]): dt_remote=save.datetime, ) self.saves.append(rsave) - self.signals.widget.update.emit() + self.signals.widget.refresh.emit() def update_saves(self): """Use only in a thread""" diff --git a/rare/models/signals.py b/rare/models/signals.py index 1e65b19513..bed9c8ae6e 100644 --- a/rare/models/signals.py +++ b/rare/models/signals.py @@ -3,59 +3,68 @@ from .install import InstallOptionsModel, UninstallOptionsModel +class GlobalSignalsApplicationSignals(QObject): + # int: exit code + quit = Signal(int) + # str: title, str: body + notify = Signal(str, str) + # none + update_tray = Signal() + # none + update_statusbar = Signal() + # str: locale + # change_translation = Signal(str) + # none + update_game_tags = Signal() + + +class GlobalSignalsGameSignals(QObject): + # model + install = Signal(InstallOptionsModel) + # str: app_name + installed = Signal(str) + # model + uninstall = Signal(UninstallOptionsModel) + # str: app_name + uninstalled = Signal(str) + + +class GlobalSignalsDownloadSignals(QObject): + # str: app_name + enqueue = Signal(str) + # str: app_name + dequeue = Signal(str) + + +class GlobalSignalsDiscordRPCSignals(QObject): + # str: app_name + update_presence = Signal(str) + # str: app_name + remove_presence = Signal(str) + # none + update_settings = Signal() + + class GlobalSignals(QObject): - class ApplicationSignals(QObject): - # int: exit code - quit = Signal(int) - # str: title, str: body - notify = Signal(str, str) - # none - update_tray = Signal() - # none - update_statusbar = Signal() - # str: locale - # change_translation = Signal(str) - # none - update_game_tags = Signal() - - class GameSignals(QObject): - # model - install = Signal(InstallOptionsModel) - # str: app_name - installed = Signal(str) - # model - uninstall = Signal(UninstallOptionsModel) - # str: app_name - uninstalled = Signal(str) - - class DownloadSignals(QObject): - # str: app_name - enqueue = Signal(str) - # str: app_name - dequeue = Signal(str) - - class DiscordRPCSignals(QObject): - # str: app_name - update_presence = Signal(str) - # str: app_name - remove_presence = Signal(str) - # none - update_settings = Signal() def __init__(self): super(GlobalSignals, self).__init__() - self.application = GlobalSignals.ApplicationSignals() - self.game = GlobalSignals.GameSignals() - self.download = GlobalSignals.DownloadSignals() - self.discord_rpc = GlobalSignals.DiscordRPCSignals() + self.application = GlobalSignalsApplicationSignals() + self.game = GlobalSignalsGameSignals() + self.download = GlobalSignalsDownloadSignals() + self.discord_rpc = GlobalSignalsDiscordRPCSignals() def deleteLater(self): + self.application.disconnect(self.application) self.application.deleteLater() del self.application + self.game.disconnect(self.game) self.game.deleteLater() del self.game + self.download.disconnect(self.download) self.download.deleteLater() del self.download + self.discord_rpc.disconnect(self.discord_rpc) self.discord_rpc.deleteLater() del self.discord_rpc super(GlobalSignals, self).deleteLater() diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py index 2371749938..388414a931 100644 --- a/rare/shared/image_manager.py +++ b/rare/shared/image_manager.py @@ -67,6 +67,8 @@ def __init__(self, func: Callable[[Game, bool], None], game: Game, force: bool): def run(self): self.func(self.game, self.force) self.signals.completed.emit(self.game) + self.signals.disconnect(self.signals) + self.signals.deleteLater() class ImageManager(QObject): diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 6a9ee74a41..0456029cf3 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -12,8 +12,8 @@ from requests.exceptions import ConnectionError, HTTPError from rare.lgndr.core import LegendaryCore -from rare.models.base_game import RareSaveGame from rare.models.game import RareEosOverlay, RareGame +from rare.models.game_slim import RareSaveGame from rare.models.settings import RareAppSettings from rare.models.signals import GlobalSignals from rare.utils import config_helper, steam_shortcuts @@ -90,30 +90,34 @@ def __init__(self, settings: RareAppSettings, args: Namespace): def enqueue_worker(self, rgame: RareGame, worker: QueueWorker): rgame.set_worker(worker) worker.feedback.started.connect(self.__signals.application.update_statusbar) - worker.feedback.finished.connect(lambda: rgame.set_worker(None)) # signals are serviced in the order they are connected, so we have to # connect the signal to update the statusbar after the one to remove the worker # from the corresponding list + worker.feedback.finished.connect(self._on_worker_finished) + worker.feedback.finished.connect(self.__signals.application.update_statusbar) + if isinstance(worker, CloudSyncWorker): - worker.feedback.finished.connect(lambda: self.workers_net.remove(worker)) - worker.feedback.finished.connect(self.__signals.application.update_statusbar) self.workers_net.append(worker) self.threadpool_net.start(worker, priority=0) elif isinstance(worker, (VerifyWorker, MoveWorker)): - worker.feedback.finished.connect(lambda: self.workers_disk.remove(worker)) - worker.feedback.finished.connect(self.__signals.application.update_statusbar) self.workers_disk.append(worker) self.threadpool_disk.start(worker, priority=0) else: raise RuntimeError(f"Cannot enqueue unkown worker type {type(worker).__name__}") self.__signals.application.update_statusbar.emit() + def _on_worker_finished(self, worker: QueueWorker): + if worker in self.workers_disk: + self.workers_disk.remove(worker) + if worker in self.workers_net: + self.workers_net.remove(worker) + def dequeue_worker(self, worker: QueueWorker): rgame = self.__library[worker.worker_info().app_name] - rgame.set_worker(None) - if worker in self.threadpool_disk: + rgame.del_worker(worker) + if worker in self.workers_disk: self.workers_disk.remove(worker) - if worker in self.threadpool_net: + if worker in self.workers_net: self.workers_net.remove(worker) self.__signals.application.update_statusbar.emit() @@ -250,6 +254,7 @@ def deleteLater(self) -> None: self.__eos_overlay = None for rgame in self.__instance.games_and_dlcs: + rgame.disconnect(rgame) rgame.deleteLater() RareCore.__instance = None super(RareCore, self).deleteLater() diff --git a/rare/shared/workers/cloud_sync.py b/rare/shared/workers/cloud_sync.py index 40751ca087..2fd9c2d4df 100644 --- a/rare/shared/workers/cloud_sync.py +++ b/rare/shared/workers/cloud_sync.py @@ -2,7 +2,7 @@ from PySide6.QtCore import QObject, Signal -from rare.models.base_game import RareGameSlim +from rare.models.game_slim import RareGameSlim from .worker import QueueWorker, QueueWorkerInfo diff --git a/rare/shared/workers/install.py b/rare/shared/workers/install.py index 31e2b73056..35b715f9ce 100644 --- a/rare/shared/workers/install.py +++ b/rare/shared/workers/install.py @@ -12,15 +12,17 @@ from .worker import Worker +class InstallInfoWorkerSignals(QObject): + result = Signal(InstallDownloadModel) + failed = Signal(str) + finished = Signal() + + class InstallInfoWorker(Worker): - class Signals(QObject): - result = Signal(InstallDownloadModel) - failed = Signal(str) - finished = Signal() def __init__(self, core: LegendaryCore, options: InstallOptionsModel): super(InstallInfoWorker, self).__init__() - self.signals = InstallInfoWorker.Signals() + self.signals = InstallInfoWorkerSignals() self.core = core self.options = options diff --git a/rare/shared/workers/move.py b/rare/shared/workers/move.py index 04cbf65a61..33ccd0be2d 100644 --- a/rare/shared/workers/move.py +++ b/rare/shared/workers/move.py @@ -25,13 +25,15 @@ class MovePathEditReasons(IndicatorReasons): MOVEDIALOG_NO_SPACE = auto() +class MoveInfoWorkerSignals(QObject): + result: Signal = Signal(bool, object, object, MovePathEditReasons) + + class MoveInfoWorker(Worker): - class Signals(QObject): - result: Signal = Signal(bool, object, object, MovePathEditReasons) def __init__(self, rgame: RareGame, igames: Iterator[RareGame], options: MoveGameModel): super(MoveInfoWorker, self).__init__() - self.signals = MoveInfoWorker.Signals() + self.signals = MoveInfoWorkerSignals() self.rgame: RareGame = rgame self.installed_games: Iterator[RareGame] = igames @@ -131,7 +133,7 @@ def worker_info(self) -> QueueWorkerInfo: def progress(self, src_size, dst_size): progress = dst_size * 100 // src_size - self.rgame.signals.progress.update.emit(progress) + self.rgame.signals.progress.refresh.emit(progress) self.signals.progress.emit(self.rgame, progress, src_size, dst_size) def run_real(self): diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py index 5407d6edd8..61ea760958 100644 --- a/rare/shared/workers/uninstall.py +++ b/rare/shared/workers/uninstall.py @@ -95,13 +95,15 @@ def uninstall_game( return status.success, status.message +class UninstallWorkerSignals(QObject): + result = Signal(RareGame, bool, str) + + class UninstallWorker(Worker): - class Signals(QObject): - result = Signal(RareGame, bool, str) def __init__(self, core: LegendaryCore, rgame: RareGame, options: UninstallOptionsModel): super(UninstallWorker, self).__init__() - self.signals = UninstallWorker.Signals() + self.signals = UninstallWorkerSignals() self.core = core self.rgame = rgame self.options = options diff --git a/rare/shared/workers/verify.py b/rare/shared/workers/verify.py index 01e0732e0d..b82305c9fa 100644 --- a/rare/shared/workers/verify.py +++ b/rare/shared/workers/verify.py @@ -31,7 +31,7 @@ def __init__(self, core: LegendaryCore, args: Namespace, rgame: RareGame): self.rgame.state = RareGame.State.VERIFYING def __status_callback(self, num: int, total: int, percentage: float, speed: float): - self.rgame.signals.progress.update.emit(num * 100 // total) + self.rgame.signals.progress.refresh.emit(num * 100 // total) self.signals.progress.emit(self.rgame, num, total, percentage, speed) def worker_info(self) -> QueueWorkerInfo: diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py index 967f2f64a9..c6bc627231 100644 --- a/rare/shared/workers/wine_resolver.py +++ b/rare/shared/workers/wine_resolver.py @@ -25,13 +25,15 @@ from rare.utils.compat import utils as compat_utils +class WinePathResolverSignals(QObject): + result_ready = Signal(str, str) + + class WinePathResolver(Worker): - class Signals(QObject): - result_ready = Signal(str, str) def __init__(self, core: LegendaryCore, app_name: str, path: str): super(WinePathResolver, self).__init__() - self.signals = WinePathResolver.Signals() + self.signals = WinePathResolverSignals() self.core = core self.app_name = app_name self.path = path diff --git a/rare/shared/workers/worker.py b/rare/shared/workers/worker.py index 1ddb6e35b9..1d12651a3a 100644 --- a/rare/shared/workers/worker.py +++ b/rare/shared/workers/worker.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from enum import IntEnum from logging import Logger, getLogger -from typing import Optional from PySide6.QtCore import QObject, QRunnable, Signal, Slot @@ -21,7 +20,10 @@ def __init__(self): super(Worker, self).__init__() self.setAutoDelete(True) self.__logger = getLogger(type(self).__name__) - self.__signals: Optional[QObject] = None + self.__signals: QObject = None + + def __str__(self): + return type(self).__name__ @property def logger(self) -> Logger: @@ -44,6 +46,7 @@ def run_real(self): @Slot() def run(self): self.run_real() + self.signals.disconnect(self.signals) self.signals.deleteLater() @@ -63,6 +66,13 @@ class QueueWorkerInfo: progress: int = 0 +class QueueWorkerSignals(QObject): + # object: worker object + started = Signal(object) + # object: worker object + finished = Signal(object) + + class QueueWorker(Worker): """ Base queueable worker class @@ -74,22 +84,20 @@ class QueueWorker(Worker): to the `QueueWorker.signals` attribute, implement `QueueWorker.run_real()` and `QueueWorker.worker_info()` """ - class Signals(QObject): - started = Signal() - finished = Signal() - def __init__(self): super(QueueWorker, self).__init__() - self.feedback = QueueWorker.Signals() + self.feedback = QueueWorkerSignals() self.state = QueueWorkerState.QUEUED self._kill = False @Slot() def run(self): self.state = QueueWorkerState.ACTIVE - self.feedback.started.emit() + self.feedback.started.emit(self) super(QueueWorker, self).run() - self.feedback.finished.emit() + self.feedback.finished.emit(self) + self.feedback.started.disconnect() + self.feedback.finished.disconnect() self.feedback.deleteLater() @abstractmethod diff --git a/rare/ui/components/tabs/settings/about.py b/rare/ui/components/tabs/settings/about.py index abb5a9d050..37a7fa73a4 100644 --- a/rare/ui/components/tabs/settings/about.py +++ b/rare/ui/components/tabs/settings/about.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'about.ui' ## -## Created by: Qt User Interface Compiler version 6.9.1 +## Created by: Qt User Interface Compiler version 6.10.1 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -17,10 +17,10 @@ class Ui_About(object): def setupUi(self, About): if not About.objectName(): About.setObjectName(u"About") - About.resize(507, 210) + About.resize(508, 210) self.about_layout = QFormLayout(About) self.about_layout.setObjectName(u"about_layout") - self.about_layout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + self.about_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignTrailing|Qt.AlignmentFlag.AlignVCenter) self.version_label = QLabel(About) self.version_label.setObjectName(u"version_label") font = QFont() @@ -41,11 +41,11 @@ def setupUi(self, About): self.about_layout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.update_label) - self.update_lbl = QLabel(About) - self.update_lbl.setObjectName(u"update_lbl") - self.update_lbl.setText(u"error") + self.update_field = QLabel(About) + self.update_field.setObjectName(u"update_field") + self.update_field.setText(u"error") - self.about_layout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.update_lbl) + self.about_layout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.update_field) self.open_browser = QPushButton(About) self.open_browser.setObjectName(u"open_browser") diff --git a/rare/ui/components/tabs/settings/about.ui b/rare/ui/components/tabs/settings/about.ui index 967d702965..f2d797174f 100644 --- a/rare/ui/components/tabs/settings/about.ui +++ b/rare/ui/components/tabs/settings/about.ui @@ -6,7 +6,7 @@ 0 0 - 507 + 508 210 @@ -15,13 +15,12 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - 75 true @@ -41,7 +40,6 @@ - 75 true @@ -51,7 +49,7 @@ - + error @@ -74,7 +72,6 @@ - 75 true @@ -102,7 +99,6 @@ - 75 true @@ -128,7 +124,6 @@ - 75 true @@ -148,7 +143,6 @@ - 75 true diff --git a/rare/utils/compat/steam.py b/rare/utils/compat/steam.py index 9a32962ec8..9258c7ecd9 100644 --- a/rare/utils/compat/steam.py +++ b/rare/utils/compat/steam.py @@ -8,6 +8,8 @@ import vdf +from rare.utils.paths import data_dir + logger = getLogger("SteamTools") steam_client_install_paths = [os.path.expanduser("~/.local/share/Steam")] @@ -254,10 +256,12 @@ def find_steam_tools(steam_path: str, library: str) -> List[ProtonTool]: def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: compatibilitytools_paths = { - "/usr/share/steam/compatibilitytools.d", + data_dir().joinpath("compatibilitytools").as_posix(), + os.path.expanduser("~/.local/share/umu/compatibilitytools"), os.path.expanduser(os.path.join(steam_path, "compatibilitytools.d")), os.path.expanduser("~/.steam/compatibilitytools.d"), os.path.expanduser("~/.steam/root/compatibilitytools.d"), + "/usr/share/steam/compatibilitytools.d", } compatibilitytools_paths = {os.path.realpath(path) for path in compatibilitytools_paths if os.path.isdir(path)} tools = [] diff --git a/rare/utils/compat/wine.py b/rare/utils/compat/wine.py index 998b142ba4..c92189f4b5 100644 --- a/rare/utils/compat/wine.py +++ b/rare/utils/compat/wine.py @@ -19,6 +19,7 @@ def find_lutris() -> Tuple[str, str]: if os.path.isdir(path) and os.path.isdir(runtime_path) and os.path.isdir(wine_path): __lutris_runtime, __lutris_wine = runtime_path, wine_path return runtime_path, wine_path + return "", "" @dataclass @@ -40,6 +41,7 @@ def find_lutris_wines(runtime_path: str = None, wine_path: str = None) -> List[W runners = [] if not runtime_path and not wine_path: return runners + return runners def __get_lib_path(executable: str, basename: str = "") -> str: diff --git a/rare/utils/misc.py b/rare/utils/misc.py index e81cb4bda4..97cd661216 100644 --- a/rare/utils/misc.py +++ b/rare/utils/misc.py @@ -1,3 +1,4 @@ +import functools import os from datetime import UTC, datetime from enum import IntEnum @@ -214,6 +215,16 @@ def qta_icon(icn_str: str, fallback: str = None, **kwargs): return qtawesome.icon("ei.error", **kwargs) +# Source - https://stackoverflow.com/a +# Posted by benrg +# Retrieved 2025-12-25, License - CC BY-SA 4.0 + +def partial_bound_method(bound_method, *args, **kwargs): + f = functools.partialmethod(bound_method.__func__, *args, **kwargs) + # NB: the seemingly redundant lambda is needed to ensure the correct result type + return (lambda *args: f(*args)).__get__(bound_method.__self__) + + def widget_object_name(widget: Union[QObject, ShibokenObject, Type], suffix: str) -> str: suffix = f"_{suffix}" if suffix else "" if isinstance(widget, QObject): diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py index a9a0431592..74b6bb7700 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qt_requests.py @@ -121,9 +121,7 @@ def __on_finished(self, reply: QNetworkReply): item = self.__active_requests.pop(reply, None) if item is None: self.logger.error("QNetworkReply: %s without associated item", reply.url().toString()) - reply.deleteLater() - return - if reply.error() != QNetworkReply.NetworkError.NoError: + elif reply.error() != QNetworkReply.NetworkError.NoError: self.logger.error(reply.errorString()) else: mimetype, charset = self.__parse_content_type(reply.header(QNetworkRequest.KnownHeaders.ContentTypeHeader)) @@ -137,4 +135,5 @@ def __on_finished(self, reply: QNetworkReply): data = None for handler in item.handlers: handler(data) + reply.disconnect(reply) reply.deleteLater() diff --git a/rare/utils/slot_adapters.py b/rare/utils/slot_adapters.py new file mode 100644 index 0000000000..3e97ca2b9c --- /dev/null +++ b/rare/utils/slot_adapters.py @@ -0,0 +1,72 @@ +import types +from typing import Callable + +from PySide6 import QtCore, QtGui, QtWidgets + + +class CallableSlotAdapter(QtCore.QObject): + """A QObject that calls a python callable whenever its slot is called. + + :param parent: The required parent QObject that manages this + instance lifecycle through the Qt parent-child relationship. + :param fn: The Python callable to call. + + Connect the desired signal to self.slot. + """ + + def __init__(self, parent, fn): + self._fn = fn + super().__init__(parent) + + def slot(self, *args): + code = self._fn.__code__ + co_argcount = code.co_argcount + if args and isinstance(self._fn, types.MethodType): + args -= 1 + self._fn(*args[:co_argcount]) + + +class CallableAction(QtGui.QAction): + """An action that calls a python callable. + + :param parent: The parent for this action. If parent is a QMenu instance, + or QActionGroup who has a QMenu instance parent, + then automatically call parent.addAction(self). + :param text: The text to display for the action. + :param fn: The callable that is called with each trigger. + :param checkable: True to allow checkable. + :param checked: Set the checked state, if checkable. + + As of Feb 2024, connecting a callable to a signal increments + the callable's reference count. Unfortunately, this reference + count is not decremented when the object is deleted. + + This class provides a Python-side wrapper for a QAction that + connects to a Python callable. You can safely provide a lambda + which will be dereferenced and deleted along with the QAction. + + For a discussion on PySide6 memory management, see + https://forum.qt.io/topic/154590/pyside6-memory-model-and-qobject-lifetime-management/11 + """ + + def __init__(self, parent: QtCore.QObject, text: str, fn: Callable, checkable=False, checked=False): + self._fn = fn + super().__init__(text, parent=parent) + if bool(checkable): + self.setCheckable(True) + self.setChecked(bool(checked)) + self.triggered.connect(self._on_triggered) + while isinstance(parent, QtGui.QActionGroup): + parent = parent.parent() + if isinstance(parent, QtWidgets.QMenu): + parent.addAction(self) + + def _on_triggered(self, checked=False): + code = self._fn.__code__ + args = code.co_argcount + if args and isinstance(self._fn, types.MethodType): + args -= 1 + if args == 0: + self._fn() + elif args == 1: + self._fn(checked) diff --git a/rare/widgets/button_edit.py b/rare/widgets/button_edit.py index e5362044da..505aff6ece 100644 --- a/rare/widgets/button_edit.py +++ b/rare/widgets/button_edit.py @@ -15,7 +15,7 @@ def __init__(self, icon_name, placeholder_text: str, parent=None): self.button.setObjectName(f"{type(self).__name__}Button") self.button.setIcon(qta_icon(icon_name)) self.button.setCursor(Qt.CursorShape.ArrowCursor) - self.button.clicked.connect(self.buttonClicked.emit) + self.button.clicked.connect(self.buttonClicked) self.setPlaceholderText(placeholder_text) # frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) diff --git a/rare/widgets/image_widget.py b/rare/widgets/image_widget.py index 2ad859060e..8315dc2038 100644 --- a/rare/widgets/image_widget.py +++ b/rare/widgets/image_widget.py @@ -29,16 +29,17 @@ class Border(Enum): Rounded = 0 Squared = 1 - _pixmap: Optional[QPixmap] = None - _opacity: float = 1.0 - _transform: QTransform - _smooth_transform: bool = False _rounded_overlay: Optional[OverlayPath] = None _squared_overlay: Optional[OverlayPath] = None - _image_size: Optional[ImageSize.Preset] = None def __init__(self, parent=None) -> None: super(ImageWidget, self).__init__(parent=parent) + self._pixmap: Optional[QPixmap] = None + self._opacity: float = 1.0 + self._transform: QTransform = None + self._smooth_transform: bool = False + self._image_size: Optional[ImageSize.Preset] = None + self.setObjectName(type(self).__name__) self.setContentsMargins(0, 0, 0, 0) self.paint_image = self.paint_image_empty @@ -187,8 +188,8 @@ def __init__(self, manager: QtRequests, parent=None): self.spinner.setVisible(False) def fetchPixmap(self, url: str, params: Dict = None): - self.spinner.start() self.spinner.setFixedSize(self._image_size.size) + self.spinner.start() params = { "resize": 1, "w": self._image_size.base.size.width(), diff --git a/rare/widgets/indicator_edit.py b/rare/widgets/indicator_edit.py index 09a5525b63..69b3b07f8f 100644 --- a/rare/widgets/indicator_edit.py +++ b/rare/widgets/indicator_edit.py @@ -99,20 +99,22 @@ def extend(self, reasons: Dict): self.__text.update(reasons) -class EditFuncRunnable(QRunnable): - class Signals(QObject): - result = Signal(bool, str, int) +class EditFuncRunnableSignals(QObject): + result = Signal(bool, str, int) + +class EditFuncRunnable(QRunnable): def __init__(self, func: Callable[[str], Tuple[bool, str, int]], args: str): super(EditFuncRunnable, self).__init__() self.setAutoDelete(True) - self.signals = EditFuncRunnable.Signals() + self.signals = EditFuncRunnableSignals() self.func = self.__wrap_edit_function(func) self.args = args def run(self): o0, o1, o2 = self.func(self.args) self.signals.result.emit(o0, o1, o2) + self.signals.disconnect(self.signals) self.signals.deleteLater() @staticmethod