From 400625975d25702eef4a162d96fcc85265788a65 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 23 Dec 2023 15:40:08 +0200 Subject: [PATCH 1/9] Dialogs: Re-implement Launch and Login dialogs on top of a few common super-classes To keep dialogs in a common format and allow them to share the same properties, three classes of dialogs have been implemented inheriting from each other. The classes are `BaseDialog` -> `ButtonDialog` -> `ActionDialog` * Basedialog: is the basis of all dialogs and is responsible for rejecting close requests from the window manager and the keyboard. It also restricts access to `exec()` and `exec_()` because they are harmful. It serves as the basis of Launch and Login dialogs * ButtonDialog: is offering buttons for accepting or rejecting the presented option. It implements its own buttons and exposes abstract methods to implement handling in them. It restricts access to `close()` because these dialogs should always product a result. It is the basis of Uninstall, Selective dialogs. * ActionDialog: in addition to the ButtonDialog, it offers an action buttom with to validate the form or to make the dialog unable to close. It serves as the basis of Install and Move dialogs. --- rare/components/__init__.py | 11 +- rare/components/dialogs/launch_dialog.py | 44 ++- rare/components/dialogs/login/__init__.py | 33 +- rare/ui/components/dialogs/launch_dialog.py | 2 +- rare/ui/components/dialogs/launch_dialog.ui | 2 +- .../components/dialogs/login/login_dialog.py | 20 +- .../components/dialogs/login/login_dialog.ui | 18 +- rare/widgets/dialogs.py | 283 ++++++++++++++++++ 8 files changed, 352 insertions(+), 61 deletions(-) create mode 100644 rare/widgets/dialogs.py diff --git a/rare/components/__init__.py b/rare/components/__init__.py index 684a1133..e120412f 100644 --- a/rare/components/__init__.py +++ b/rare/components/__init__.py @@ -76,14 +76,15 @@ class Rare(RareApp): @pyqtSlot() def launch_app(self): self.launch_dialog = LaunchDialog(parent=None) - self.launch_dialog.exit_app.connect(self.launch_dialog.close) - self.launch_dialog.exit_app.connect(self.__on_exit_app) - self.launch_dialog.start_app.connect(self.start_app) - self.launch_dialog.start_app.connect(self.launch_dialog.close) + self.launch_dialog.rejected.connect(self.__on_exit_app) + # lk: the reason we use the `start_app` signal here instead of accepted, is to keep the dialog + # until the main window has been created, then we call `accept()` to close the dialog + self.launch_dialog.start_app.connect(self.__on_start_app) + self.launch_dialog.start_app.connect(self.launch_dialog.accept) self.launch_dialog.login() @pyqtSlot() - def start_app(self): + def __on_start_app(self): self.timer = QTimer() self.timer.timeout.connect(self.re_login) self.poke_timer() diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index d231625b..e63d7a1b 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -1,25 +1,24 @@ -import platform from logging import getLogger from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtWidgets import QDialog, QApplication from requests.exceptions import ConnectionError, HTTPError from rare.components.dialogs.login import LoginDialog from rare.shared import RareCore from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog +from rare.widgets.dialogs import BaseDialog from rare.widgets.elide_label import ElideLabel logger = getLogger("LaunchDialog") -class LaunchDialog(QDialog): - exit_app = pyqtSignal(int) +class LaunchDialog(BaseDialog): + # lk: the reason we use the `start_app` signal here instead of accepted, is to keep the dialog + # until the main window has been created, then we call `accept()` to close the dialog start_app = pyqtSignal() def __init__(self, parent=None): super(LaunchDialog, self).__init__(parent=parent) - self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags( Qt.Window | Qt.Dialog @@ -29,12 +28,10 @@ class LaunchDialog(QDialog): | Qt.WindowMinimizeButtonHint | Qt.MSWindowsFixedSizeDialogHint ) - self.setWindowModality(Qt.WindowModal) + self.ui = Ui_LaunchDialog() self.ui.setupUi(self) - self.reject_close = True - self.progress_info = ElideLabel(parent=self) self.progress_info.setFixedHeight(False) self.ui.launch_layout.addWidget(self.progress_info) @@ -46,9 +43,11 @@ class LaunchDialog(QDialog): self.args = self.rcore.args() self.login_dialog = LoginDialog(core=self.core, parent=parent) + self.login_dialog.rejected.connect(self.reject) + self.login_dialog.accepted.connect(self.do_launch) def login(self): - do_launch = True + can_launch = True try: if self.args.offline: pass @@ -56,25 +55,29 @@ class LaunchDialog(QDialog): # Force an update check and notice in case there are API changes # self.core.check_for_updates(force=True) # self.core.force_show_update = True - if self.core.login(): + if self.core.login(force_refresh=True): logger.info("You are logged in") + self.login_dialog.close() else: - raise ValueError("You are not logged in. Open Login Window") + raise ValueError("You are not logged in. Opening login window.") except ValueError as e: logger.info(str(e)) # Do not set parent, because it won't show a task bar icon # Update: Inherit the same parent as LaunchDialog - do_launch = self.login_dialog.login() + can_launch = False + self.login_dialog.open() except (HTTPError, ConnectionError) as e: logger.warning(e) self.args.offline = True finally: - if do_launch: - if not self.args.silent: - self.show() - self.launch() - else: - self.exit_app.emit(0) + if can_launch: + self.do_launch() + + @pyqtSlot() + def do_launch(self): + if not self.args.silent: + self.open() + self.launch() def launch(self): self.progress_info.setText(self.tr("Preparing Rare")) @@ -87,9 +90,4 @@ class LaunchDialog(QDialog): def __on_completed(self): logger.info("App starting") - self.reject_close = False self.start_app.emit() - - def reject(self) -> None: - if not self.reject_close: - super(LaunchDialog, self).reject() diff --git a/rare/components/dialogs/login/__init__.py b/rare/components/dialogs/login/__init__.py index 42ba6452..5c2158e7 100644 --- a/rare/components/dialogs/login/__init__.py +++ b/rare/components/dialogs/login/__init__.py @@ -1,12 +1,14 @@ from logging import getLogger -from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtWidgets import QLayout, QDialog, QMessageBox, QFrame +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QLayout, QMessageBox, QFrame from legendary.core import LegendaryCore from rare.shared import ArgumentsSingleton from rare.ui.components.dialogs.login.landing_page import Ui_LandingPage from rare.ui.components.dialogs.login.login_dialog import Ui_LoginDialog +from rare.utils.misc import icon +from rare.widgets.dialogs import BaseDialog from rare.widgets.sliding_stack import SlidingStackedWidget from .browser_login import BrowserLogin from .import_login import ImportLogin @@ -22,12 +24,10 @@ class LandingPage(QFrame): self.ui.setupUi(self) -class LoginDialog(QDialog): - exit_app: pyqtSignal = pyqtSignal(int) +class LoginDialog(BaseDialog): def __init__(self, core: LegendaryCore, parent=None): super(LoginDialog, self).__init__(parent=parent) - self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags( Qt.Window | Qt.Dialog @@ -38,7 +38,7 @@ class LoginDialog(QDialog): | Qt.WindowCloseButtonHint | Qt.MSWindowsFixedSizeDialogHint ) - self.setWindowModality(Qt.WindowModal) + self.ui = Ui_LoginDialog() self.ui.setupUi(self) @@ -93,13 +93,22 @@ class LoginDialog(QDialog): 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.ui.exit_button.clicked.connect(self.close) + 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.login_stack.setCurrentWidget(self.landing_page) - self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.ui.exit_button.setIcon(icon("fa.remove")) + self.ui.back_button.setIcon(icon("fa.chevron-left")) + self.ui.next_button.setIcon(icon("fa.chevron-right")) + + # lk: Set next as the default button only to stop closing the dialog when pressing enter + self.ui.exit_button.setAutoDefault(False) + self.ui.back_button.setAutoDefault(False) + self.ui.next_button.setAutoDefault(True) + + self.ui.main_layout.setSizeConstraint(QLayout.SetFixedSize) def back_clicked(self): self.ui.back_button.setEnabled(False) @@ -129,15 +138,14 @@ class LoginDialog(QDialog): def login(self): if self.args.test_start: - return False - self.exec_() - return self.logged_in + self.reject() + self.open() def login_successful(self): try: if self.core.login(): self.logged_in = True - self.close() + self.accept() else: raise ValueError("Login failed.") except Exception as e: @@ -146,3 +154,4 @@ class LoginDialog(QDialog): self.ui.next_button.setEnabled(False) self.logged_in = False QMessageBox.warning(None, self.tr("Login error"), str(e)) + diff --git a/rare/ui/components/dialogs/launch_dialog.py b/rare/ui/components/dialogs/launch_dialog.py index 9c30344e..0808126f 100644 --- a/rare/ui/components/dialogs/launch_dialog.py +++ b/rare/ui/components/dialogs/launch_dialog.py @@ -36,7 +36,7 @@ class Ui_LaunchDialog(object): def retranslateUi(self, LaunchDialog): _translate = QtCore.QCoreApplication.translate - LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching - Rare")) + LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching")) self.title_label.setText(_translate("LaunchDialog", "

Launching Rare

")) diff --git a/rare/ui/components/dialogs/launch_dialog.ui b/rare/ui/components/dialogs/launch_dialog.ui index b8a5838e..ff9f6d34 100644 --- a/rare/ui/components/dialogs/launch_dialog.ui +++ b/rare/ui/components/dialogs/launch_dialog.ui @@ -29,7 +29,7 @@ - Launching - Rare + Launching diff --git a/rare/ui/components/dialogs/login/login_dialog.py b/rare/ui/components/dialogs/login/login_dialog.py index 76eb3310..b158a420 100644 --- a/rare/ui/components/dialogs/login/login_dialog.py +++ b/rare/ui/components/dialogs/login/login_dialog.py @@ -15,38 +15,38 @@ class Ui_LoginDialog(object): def setupUi(self, LoginDialog): LoginDialog.setObjectName("LoginDialog") LoginDialog.resize(241, 128) - self.login_layout = QtWidgets.QVBoxLayout(LoginDialog) - self.login_layout.setObjectName("login_layout") + self.main_layout = QtWidgets.QVBoxLayout(LoginDialog) + self.main_layout.setObjectName("main_layout") spacerItem = QtWidgets.QSpacerItem(0, 17, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - self.login_layout.addItem(spacerItem) + self.main_layout.addItem(spacerItem) self.welcome_label = QtWidgets.QLabel(LoginDialog) self.welcome_label.setObjectName("welcome_label") - self.login_layout.addWidget(self.welcome_label) + self.main_layout.addWidget(self.welcome_label) spacerItem1 = QtWidgets.QSpacerItem(0, 17, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - self.login_layout.addItem(spacerItem1) + self.main_layout.addItem(spacerItem1) self.login_stack_layout = QtWidgets.QVBoxLayout() self.login_stack_layout.setObjectName("login_stack_layout") - self.login_layout.addLayout(self.login_stack_layout) + self.main_layout.addLayout(self.login_stack_layout) self.button_layout = QtWidgets.QHBoxLayout() self.button_layout.setObjectName("button_layout") - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.button_layout.addItem(spacerItem2) self.exit_button = QtWidgets.QPushButton(LoginDialog) self.exit_button.setObjectName("exit_button") self.button_layout.addWidget(self.exit_button) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.button_layout.addItem(spacerItem2) self.back_button = QtWidgets.QPushButton(LoginDialog) self.back_button.setObjectName("back_button") self.button_layout.addWidget(self.back_button) self.next_button = QtWidgets.QPushButton(LoginDialog) self.next_button.setObjectName("next_button") self.button_layout.addWidget(self.next_button) - self.login_layout.addLayout(self.button_layout) + self.main_layout.addLayout(self.button_layout) self.retranslateUi(LoginDialog) def retranslateUi(self, LoginDialog): _translate = QtCore.QCoreApplication.translate - LoginDialog.setWindowTitle(_translate("LoginDialog", "Login - Rare")) + LoginDialog.setWindowTitle(_translate("LoginDialog", "Login")) self.welcome_label.setText(_translate("LoginDialog", "

Welcome to Rare

")) self.exit_button.setText(_translate("LoginDialog", "Exit")) self.back_button.setText(_translate("LoginDialog", "Back")) diff --git a/rare/ui/components/dialogs/login/login_dialog.ui b/rare/ui/components/dialogs/login/login_dialog.ui index 5419629b..6ca92710 100644 --- a/rare/ui/components/dialogs/login/login_dialog.ui +++ b/rare/ui/components/dialogs/login/login_dialog.ui @@ -11,9 +11,9 @@ - Login - Rare + Login - + @@ -58,6 +58,13 @@ + + + + Exit + + + @@ -71,13 +78,6 @@ - - - - Exit - - - diff --git a/rare/widgets/dialogs.py b/rare/widgets/dialogs.py new file mode 100644 index 00000000..a5a97db8 --- /dev/null +++ b/rare/widgets/dialogs.py @@ -0,0 +1,283 @@ +import sys +from abc import abstractmethod + +from PyQt5.QtCore import Qt, pyqtSlot, QCoreApplication, QSize +from PyQt5.QtGui import QCloseEvent, QKeyEvent, QKeySequence +from PyQt5.QtWidgets import ( + QDialog, + QDialogButtonBox, + QApplication, + QPushButton, + QVBoxLayout, + QHBoxLayout, + QWidget, + QLayout, QSpacerItem, QSizePolicy, +) + +from rare.utils.misc import icon + + +def dialog_title_game(text: str, app_title: str) -> str: + return f"{text} '{app_title}'" + + +def dialog_title(text: str) -> str: + return f"{text} - {QCoreApplication.instance().applicationName()}" + + +class BaseDialog(QDialog): + + def __init__(self, parent=None): + super(BaseDialog, self).__init__(parent=parent) + self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) + self.setWindowModality(Qt.WindowModal) + + def setWindowTitle(self, a0): + super().setWindowTitle(dialog_title(a0)) + + def exec(self): + raise RuntimeError(f"Don't use `exec()` with {type(self).__name__}") + + def exec_(self): + raise RuntimeError(f"Don't use `exec_()` with {type(self).__name__}") + + # lk: because you will eventually find yourself back here. + # on QDialogs the Esc key closes the dialog through keyPressEvent(), + # which ultimately call `reject()`. Pressing the Enter/Return button + # is a shortcut for pressing the default button and thus calling `accept()` + # In turn both `accept()` and `reject()` evetually call `done()`. + + # In the base dialog ignore both. In the subclasses, call the method + # from QDialog if required, not this one. + # `super(BaseDialog, self).keyPressEvent(a0)` + def keyPressEvent(self, a0: QKeyEvent) -> None: + if a0.matches(QKeySequence.Cancel): + a0.ignore() + return + if a0.key() == Qt.Key_Enter or a0.key() == Qt.Key_Return: + a0.ignore() + return + super().keyPressEvent(a0) + + # Using the 'X' button on the window manager comes directly here. + # It is a spontaneous event so simply ignore it. + def closeEvent(self, a0: QCloseEvent) -> None: + if a0.spontaneous(): + a0.ignore() + return + super().closeEvent(a0) + + +class ButtonDialog(BaseDialog): + + def __init__(self, parent=None): + super(ButtonDialog, self).__init__(parent=parent) + + self.reject_button = QPushButton(self) + self.reject_button.setText(self.tr("Cancel")) + self.reject_button.setIcon(icon("fa.remove")) + self.reject_button.setAutoDefault(False) + self.reject_button.clicked.connect(self.reject) + + self.accept_button = QPushButton(self) + self.accept_button.setAutoDefault(False) + self.accept_button.clicked.connect(self.accept) + + self.button_layout = QHBoxLayout() + self.button_layout.addWidget(self.reject_button) + self.button_layout.addStretch(20) + self.button_layout.addStretch(1) + self.button_layout.addWidget(self.accept_button) + + self.main_layout = QVBoxLayout(self) + # lk: dirty way to set a minimum width with fixed size constraint + spacer = QSpacerItem( + 480, self.main_layout.spacing(), + QSizePolicy.Expanding, QSizePolicy.Fixed + ) + self.main_layout.addItem(spacer) + self.main_layout.addLayout(self.button_layout) + self.main_layout.setSizeConstraint(QLayout.SetFixedSize) + + def close(self): + raise RuntimeError(f"Don't use `close()` with {type(self).__name__}") + + def setCentralWidget(self, widget: QWidget): + widget.layout().setContentsMargins(0, 0, 0, 0) + self.main_layout.insertWidget(0, widget) + + def setCentralLayout(self, layout: QLayout): + layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.insertLayout(0, layout) + + @abstractmethod + def accept_handler(self): + raise NotImplementedError + + @abstractmethod + def reject_handler(self): + raise NotImplementedError + + @abstractmethod + def done_handler(self): + raise NotImplementedError + + # These only apply to QDialog. If we move to QWidget for embedded dialogs + # we have to use close() and custom handling. + + # lk: Override accept to run our abstract handling method + def accept(self): + self.accept_handler() + super().accept() + + # lk: Override reject to run our abstract handling method + def reject(self): + self.reject_handler() + super().reject() + + # lk: Override `done()` to to run our abstract handling method + def done(self, a0): + self.done_handler() + super().done(a0) + + # lk: Ignore BaseDialog::keyPressEvent and call QDialog::keyPressEvent + # because we handle accept and reject here. + def keyPressEvent(self, a0: QKeyEvent) -> None: + super(BaseDialog, self).keyPressEvent(a0) + + # lk: Ignore BaseDialog::closeEvent and call QDialog::closeEvent + # because we handle accept and reject here. + def closeEvent(self, a0: QCloseEvent) -> None: + super(BaseDialog, self).closeEvent(a0) + + +class ActionDialog(ButtonDialog): + def __init__(self, parent=None): + super(ActionDialog, self).__init__(parent=parent) + self.__reject_close = False + + self.action_button = QPushButton(self) + self.action_button.setAutoDefault(True) + self.action_button.clicked.connect(self.action) + + self.button_layout.insertWidget(2, self.action_button) + + def active(self) -> bool: + return self.__reject_close + + def setActive(self, active: bool): + self.reject_button.setDisabled(active) + self.action_button.setDisabled(active) + self.accept_button.setDisabled(active) + self.__reject_close = active + + @abstractmethod + def action_handler(self): + raise NotImplementedError + + @pyqtSlot() + def action(self): + self.setActive(True) + self.action_handler() + + # lk: Ignore all key presses if there is an ongoing action + def keyPressEvent(self, a0: QKeyEvent) -> None: + if self.__reject_close: + a0.ignore() + return + super(BaseDialog, self).keyPressEvent(a0) + + # lk: Ignore all closeEvents if there is an ongoing action + def closeEvent(self, a0: QCloseEvent) -> None: + if self.__reject_close: + a0.ignore() + return + super(BaseDialog, self).closeEvent(a0) + + +__all__ = ["dialog_title", "dialog_title_game", "BaseDialog", "ButtonDialog", "ActionDialog"] + + +class TestDialog(BaseDialog): + def __init__(self, parent=None): + super(TestDialog, self).__init__(parent=parent) + + self.accept_button = QPushButton("accept", self) + self.reject_button = QPushButton("reject", self) + self.action_button = QPushButton("action", self) + self.button_box = QDialogButtonBox(Qt.Horizontal, self) + self.button_box.addButton(self.accept_button, QDialogButtonBox.AcceptRole) + self.button_box.addButton(self.reject_button, QDialogButtonBox.RejectRole) + self.button_box.addButton(self.action_button, QDialogButtonBox.ActionRole) + + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + layout = QVBoxLayout(self) + layout.addWidget(self.button_box) + + self.setMinimumWidth(480) + + def setWindowTitle(self, a0): + super().setWindowTitle(dialog_title(a0)) + + def close(self): + print("in close") + super().close() + + def closeEvent(self, a0: QCloseEvent) -> None: + print("in closeEvent") + if a0.spontaneous(): + print("is spontaneous") + a0.ignore() + return + if self.reject_close: + a0.ignore() + else: + self._on_close() + super().closeEvent(a0) + # super().closeEvent(a0) + + def done(self, a0): + print(f"in done {a0}") + return + super().done(a0) + + def accept(self): + print("in accept") + self._on_accept() + # return + # super().accept() + + def reject(self): + print("in reject") + self._on_reject() + # return + # super().reject() + + def _on_close(self): + print("in _on_close") + + def _on_accept(self): + print("in _on_accepted") + # self.close() + + def _on_reject(self): + print("in _on_rejected") + self.close() + + def keyPressEvent(self, a0: QKeyEvent) -> None: + super(BaseDialog, self).keyPressEvent(a0) + + +def test_dialog(): + app = QApplication(sys.argv) + dlg = TestDialog(None) + dlg.show() + ret = app.exec() + sys.exit(ret) + + +if __name__ == "__main__": + test_dialog() From 616df235cd0cdc140190be552f95ef1529c11d32 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 23 Dec 2023 15:40:35 +0200 Subject: [PATCH 2/9] ListWidget: Use line variant for qta icon --- rare/components/tabs/games/game_widgets/list_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rare/components/tabs/games/game_widgets/list_widget.py b/rare/components/tabs/games/game_widgets/list_widget.py index d06cd8fc..32f5dbd2 100644 --- a/rare/components/tabs/games/game_widgets/list_widget.py +++ b/rare/components/tabs/games/game_widgets/list_widget.py @@ -40,7 +40,7 @@ class ListWidget(object): self.install_btn = QPushButton(parent=widget) self.install_btn.setObjectName(f"{type(self).__name__}Button") - self.install_btn.setIcon(icon("ri.install-fill")) + self.install_btn.setIcon(icon("ri.install-line")) self.install_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.install_btn.setFixedWidth(120) From 1f34ad4b1390da1b4ba4297f5880977d4796316f Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 14 Sep 2023 15:46:26 +0300 Subject: [PATCH 3/9] ProtonSettings: Move proton search function into `utils/proton.py` --- .../tabs/settings/widgets/proton.py | 28 ++----------------- rare/utils/proton.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 rare/utils/proton.py diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py index 110db4ab..658aaf87 100644 --- a/rare/components/tabs/settings/widgets/proton.py +++ b/rare/components/tabs/settings/widgets/proton.py @@ -9,37 +9,13 @@ from PyQt5.QtWidgets import QGroupBox, QFileDialog from rare.components.tabs.settings import LinuxSettings from rare.shared import LegendaryCoreSingleton from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings -from rare.utils import config_helper +from rare.utils import config_helper, proton from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon from .wrapper import WrapperSettings logger = getLogger("Proton") -def find_proton_combos(): - possible_proton_combos = [] - compatibilitytools_dirs = [ - os.path.expanduser("~/.steam/steam/steamapps/common"), - "/usr/share/steam/compatibilitytools.d", - os.path.expanduser("~/.steam/compatibilitytools.d"), - os.path.expanduser("~/.steam/root/compatibilitytools.d"), - ] - for c in compatibilitytools_dirs: - if os.path.exists(c): - for i in os.listdir(c): - proton = os.path.join(c, i, "proton") - compatibilitytool = os.path.join(c, i, "compatibilitytool.vdf") - toolmanifest = os.path.join(c, i, "toolmanifest.vdf") - if os.path.exists(proton) and ( - os.path.exists(compatibilitytool) or os.path.exists(toolmanifest) - ): - wrapper = f'"{proton}" run' - possible_proton_combos.append(wrapper) - if not possible_proton_combos: - logger.warning("Unable to find any Proton version") - return possible_proton_combos - - class ProtonSettings(QGroupBox): # str: option key environ_changed = pyqtSignal(str) @@ -53,7 +29,7 @@ class ProtonSettings(QGroupBox): self._linux_settings = linux_settings self._wrapper_settings = wrapper_settings self.core = LegendaryCoreSingleton() - self.possible_proton_combos = find_proton_combos() + self.possible_proton_combos = proton.find_proton_combos() self.ui.proton_combo.addItems(self.possible_proton_combos) self.ui.proton_combo.currentIndexChanged.connect(self.change_proton) diff --git a/rare/utils/proton.py b/rare/utils/proton.py new file mode 100644 index 00000000..371f2133 --- /dev/null +++ b/rare/utils/proton.py @@ -0,0 +1,28 @@ +import os +from logging import getLogger + +logger = getLogger("Proton") + + +def find_proton_combos(): + possible_proton_combos = [] + compatibilitytools_dirs = [ + os.path.expanduser("~/.steam/steam/steamapps/common"), + "/usr/share/steam/compatibilitytools.d", + os.path.expanduser("~/.steam/compatibilitytools.d"), + os.path.expanduser("~/.steam/root/compatibilitytools.d"), + ] + for c in compatibilitytools_dirs: + if os.path.exists(c): + for i in os.listdir(c): + proton = os.path.join(c, i, "proton") + compatibilitytool = os.path.join(c, i, "compatibilitytool.vdf") + toolmanifest = os.path.join(c, i, "toolmanifest.vdf") + if os.path.exists(proton) and ( + os.path.exists(compatibilitytool) or os.path.exists(toolmanifest) + ): + wrapper = f'"{proton}" run' + possible_proton_combos.append(wrapper) + if not possible_proton_combos: + logger.warning("Unable to find any Proton version") + return possible_proton_combos From efe9031211e899299b648eec725062166b7b9ba1 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:24:53 +0300 Subject: [PATCH 4/9] Proton: Add functions to find and integrate compatibilty tools and runtimes --- .../tabs/settings/widgets/env_vars_model.py | 3 +- rare/utils/proton.py | 243 ++++++++++++++++++ requirements.txt | 1 + 3 files changed, 246 insertions(+), 1 deletion(-) diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/env_vars_model.py index 11e16449..795533e7 100644 --- a/rare/components/tabs/settings/widgets/env_vars_model.py +++ b/rare/components/tabs/settings/widgets/env_vars_model.py @@ -8,6 +8,7 @@ from PyQt5.QtGui import QFont from rare.lgndr.core import LegendaryCore from rare.utils.misc import icon +from rare.utils import proton class EnvVarsTableModel(QAbstractTableModel): @@ -23,11 +24,11 @@ class EnvVarsTableModel(QAbstractTableModel): self.__readonly = [ "STEAM_COMPAT_DATA_PATH", - "STEAM_COMPAT_CLIENT_INSTALL_PATH", "WINEPREFIX", "DXVK_HUD", "MANGOHUD_CONFIG", ] + self.__readonly.extend(proton.get_steam_environment(None).keys()) self.__default: str = "default" self.__appname: str = None diff --git a/rare/utils/proton.py b/rare/utils/proton.py index 371f2133..aa4ec7c3 100644 --- a/rare/utils/proton.py +++ b/rare/utils/proton.py @@ -1,8 +1,251 @@ import os +from dataclasses import dataclass from logging import getLogger +from typing import Optional, Union, List, Dict + +import vdf logger = getLogger("Proton") +steam_compat_client_install_paths = [os.path.expanduser("~/.local/share/Steam")] + + +def find_steam() -> str: + # return the first valid path + for path in steam_compat_client_install_paths: + if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")): + return path + + +def find_libraries(steam_path: str) -> List[str]: + vdf_path = os.path.join(steam_path, "steamapps", "libraryfolders.vdf") + with open(vdf_path, "r") as f: + libraryfolders = vdf.load(f)["libraryfolders"] + libraries = [os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()] + return libraries + + +@dataclass +class SteamBase: + steam_path: str + tool_path: str + toolmanifest: dict + + def __eq__(self, other): + return self.tool_path == other.tool_path + + def __hash__(self): + return hash(self.tool_path) + + def commandline(self): + cmd = "".join([f"'{self.tool_path}'", self.toolmanifest["manifest"]["commandline"]]) + cmd = os.path.normpath(cmd) + # NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff + cmd = cmd.replace("%verb%", "waitforexitandrun") + return cmd + + +@dataclass +class SteamRuntime(SteamBase): + steam_library: str + appmanifest: dict + + def name(self): + return self.appmanifest["AppState"]["name"] + + def appid(self): + return self.appmanifest["AppState"]["appid"] + + +@dataclass +class ProtonTool(SteamRuntime): + runtime: SteamRuntime = None + + def __bool__(self): + if appid := self.toolmanifest.get("require_tool_appid", False): + return self.runtime is not None and self.runtime.appid() == appid + + def commandline(self): + runtime_cmd = self.runtime.commandline() + cmd = super().commandline() + return " ".join([runtime_cmd, cmd]) + + +@dataclass +class CompatibilityTool(SteamBase): + compatibilitytool: dict + runtime: SteamRuntime = None + + def __bool__(self): + if appid := self.toolmanifest.get("require_tool_appid", False): + return self.runtime is not None and self.runtime.appid() == appid + + def name(self): + name, data = list(self.compatibilitytool["compatibilitytools"]["compat_tools"].items())[0] + return data["display_name"] + + def commandline(self): + runtime_cmd = self.runtime.commandline() if self.runtime is not None else "" + cmd = super().commandline() + return " ".join([runtime_cmd, cmd]) + + +def find_appmanifests(library: str) -> List[dict]: + appmanifests = [] + for entry in os.scandir(library): + if entry.is_file() and entry.name.endswith(".acf"): + with open(os.path.join(library, entry.name), "r") as f: + appmanifest = vdf.load(f) + appmanifests.append(appmanifest) + return appmanifests + + +def find_protons(steam_path: str, library: str) -> List[ProtonTool]: + protons = [] + appmanifests = find_appmanifests(library) + common = os.path.join(library, "common") + for appmanifest in appmanifests: + folder = appmanifest["AppState"]["installdir"] + tool_path = os.path.join(common, folder) + if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): + with open(vdf_file, "r") as f: + toolmanifest = vdf.load(f) + if toolmanifest["manifest"]["compatmanager_layer_name"] == "proton": + protons.append( + ProtonTool( + steam_path=steam_path, + steam_library=library, + appmanifest=appmanifest, + tool_path=tool_path, + toolmanifest=toolmanifest, + ) + ) + return protons + + +def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: + compatibilitytools_paths = { + "/usr/share/steam/compatibilitytools.d", + os.path.expanduser(os.path.join(steam_path, "compatibilitytools.d")), + os.path.expanduser("~/.steam/compatibilitytools.d"), + os.path.expanduser("~/.steam/root/compatibilitytools.d"), + } + compatibilitytools_paths = { + os.path.realpath(path) for path in compatibilitytools_paths if os.path.isdir(path) + } + tools = [] + for path in compatibilitytools_paths: + for entry in os.scandir(path): + if entry.is_dir(): + tool_path = os.path.join(path, entry.name) + tool_vdf = os.path.join(tool_path, "compatibilitytool.vdf") + manifest_vdf = os.path.join(tool_path, "toolmanifest.vdf") + if os.path.isfile(tool_vdf) and os.path.isfile(manifest_vdf): + with open(tool_vdf, "r") as f: + compatibilitytool = vdf.load(f) + with open(manifest_vdf, "r") as f: + manifest = vdf.load(f) + tools.append( + CompatibilityTool( + steam_path=steam_path, + tool_path=tool_path, + toolmanifest=manifest, + compatibilitytool=compatibilitytool, + ) + ) + return tools + + +def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]: + runtimes = {} + appmanifests = find_appmanifests(library) + common = os.path.join(library, "common") + for appmanifest in appmanifests: + folder = appmanifest["AppState"]["installdir"] + tool_path = os.path.join(common, folder) + if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): + with open(vdf_file, "r") as f: + toolmanifest = vdf.load(f) + if toolmanifest["manifest"]["compatmanager_layer_name"] == "container-runtime": + runtimes.update( + { + appmanifest["AppState"]["appid"]: SteamRuntime( + steam_path=steam_path, + steam_library=library, + appmanifest=appmanifest, + tool_path=tool_path, + toolmanifest=toolmanifest, + ) + } + ) + return runtimes + + +def find_runtime( + tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime] +) -> Optional[SteamRuntime]: + required_tool = tool.toolmanifest["manifest"].get("require_tool_appid") + if required_tool is None: + return None + return runtimes[required_tool] + + +def get_steam_environment(tool: Optional[Union[ProtonTool, CompatibilityTool]], app_name: str = None) -> Dict: + environ = {} + # If the tool is unset, return all affected env variable names + # IMPORTANT: keep this in sync with the code below + if tool is None: + environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "" + environ["STEAM_COMPAT_LIBRARY_PATHS"] = "" + environ["STEAM_COMPAT_MOUNTS"] = "" + environ["STEAM_COMPAT_TOOL_PATHS"] = "" + return environ + + environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = tool.steam_path + if isinstance(tool, ProtonTool): + environ["STEAM_COMPAT_LIBRARY_PATHS"] = tool.steam_library + if tool.runtime is not None: + compat_mounts = [tool.tool_path, tool.runtime.tool_path] + environ["STEAM_COMPAT_MOUNTS"] = ":".join(compat_mounts) + tool_paths = [tool.tool_path] + if tool.runtime is not None: + tool_paths.append(tool.runtime.tool_path) + environ["STEAM_COMPAT_TOOL_PATHS"] = ":".join(tool_paths) + return environ + + +def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: + steam_path = find_steam() + logger.info("Using Steam install in %s", steam_path) + steam_libraries = find_libraries(steam_path) + logger.info("Searching for tools in libraries %s", steam_libraries) + + runtimes = {} + for library in steam_libraries: + runtimes.update(find_runtimes(steam_path, library)) + + tools = [] + for library in steam_libraries: + tools.extend(find_protons(steam_path, library)) + tools.extend(find_compatibility_tools(steam_path)) + + for tool in tools: + runtime = find_runtime(tool, runtimes) + tool.runtime = runtime + + return tools + + +if __name__ == "__main__": + from pprint import pprint + + _tools = find_tools() + pprint(_tools) + + for tool in _tools: + print(get_steam_environment(tool)) + print(tool.name(), tool.commandline()) + def find_proton_combos(): possible_proton_combos = [] diff --git a/requirements.txt b/requirements.txt index cf445de5..7301263b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ QtAwesome setuptools legendary-gl>=0.20.34 orjson +vdf; platform_system != "Windows" pywin32; platform_system == "Windows" From f6a372cc1f8bacd08670b23d9bc9d9bb4448db0a Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:47:35 +0300 Subject: [PATCH 5/9] ConfigHelper: Add wine related functions `get_wine_prefixes`: Returns all prefixes defined in legendary's configuration `get_wine_prefix`: Returns wine prefix associated with a game --- rare/utils/config_helper.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py index aa0bc9ce..7ced748b 100644 --- a/rare/utils/config_helper.py +++ b/rare/utils/config_helper.py @@ -1,4 +1,5 @@ -from typing import Callable, Optional +import os +from typing import Callable, Optional, List, Set from legendary.core import LegendaryCore from legendary.models.config import LGDConf @@ -40,6 +41,30 @@ def remove_option(app_name, option): def remove_section(app_name): return + # Disabled due to env variables implementation if _config.has_section(app_name): _config.remove_section(app_name) save_config() + + +def get_wine_prefixes() -> Set[str]: + prefixes = ["~/.wine"] + + for name, section in _config.items(): + pfx = section.get("WINEPREFIX") or section.get("wine_prefix") + if pfx: + prefixes.append(pfx) + + return {prefix for prefix in prefixes if os.path.isdir(os.path.expanduser(prefix))} + + +def get_wine_prefix(app_name: Optional[str] = None) -> str: + if app_name is None: + prefix = "~/.wine" + prefix = _config.get("default.env", "WINEPREFIX", fallback=prefix) + prefix = _config.get("default", "wine_prefix", fallback=prefix) + else: + prefix = get_wine_prefix() + prefix = _config.get(f'{app_name}.env', 'WINEPREFIX', fallback=prefix) + prefix = _config.get(app_name, 'wine_prefix', fallback=prefix) + return os.path.abspath(os.path.expanduser(prefix)) From 83328f400da9327bd527f5642dbfea032ac26488 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:55:18 +0300 Subject: [PATCH 6/9] ConfigHelper: Add generic functions to query config options with fallback * Extend `get_wine_prefix` and `get_wine_prefixes` to take Proton prefixes into account. --- rare/utils/config_helper.py | 51 ++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py index 7ced748b..909d66c4 100644 --- a/rare/utils/config_helper.py +++ b/rare/utils/config_helper.py @@ -1,5 +1,5 @@ import os -from typing import Callable, Optional, List, Set +from typing import Callable, Optional, Set, Any from legendary.core import LegendaryCore from legendary.models.config import LGDConf @@ -47,24 +47,41 @@ def remove_section(app_name): save_config() -def get_wine_prefixes() -> Set[str]: - prefixes = ["~/.wine"] +def get_game_option(option: str, app_name: Optional[str] = None, fallback: Any = None) -> str: + _option = _config.get("default", option, fallback=fallback) + if app_name is not None: + _option = _config.get(app_name, option, fallback=_option) + return _option + +def get_game_envvar(option: str, app_name: Optional[str] = None, fallback: Any = None) -> str: + _option = _config.get("default.env", option, fallback=fallback) + if app_name is not None: + _option = _config.get(f'{app_name}.env', option, fallback=_option) + return _option + + +def get_wine_prefix(app_name: Optional[str] = None, fallback: Any = None) -> str: + _prefix = os.path.join( + _config.get("default.env", "STEAM_COMPAT_DATA_PATH", fallback=fallback), "pfx") + _prefix = _config.get("default.env", "WINEPREFIX", fallback=_prefix) + _prefix = _config.get("default", "wine_prefix", fallback=_prefix) + if app_name is not None: + _prefix = os.path.join( + _config.get(f'{app_name}.env', "STEAM_COMPAT_DATA_PATH", fallback=fallback), "pfx") + _prefix = _config.get(f'{app_name}.env', 'WINEPREFIX', fallback=_prefix) + _prefix = _config.get(app_name, 'wine_prefix', fallback=_prefix) + return _prefix + + +def get_wine_prefixes() -> Set[str]: + _prefixes = [] for name, section in _config.items(): pfx = section.get("WINEPREFIX") or section.get("wine_prefix") + if not pfx: + pfx = os.path.join(compatdata, "pfx") if (compatdata := section.get("STEAM_COMPAT_DATA_PATH")) else "" if pfx: - prefixes.append(pfx) + _prefixes.append(pfx) + _prefixes = [os.path.expanduser(prefix) for prefix in _prefixes] + return {p for p in _prefixes if os.path.isdir(p)} - return {prefix for prefix in prefixes if os.path.isdir(os.path.expanduser(prefix))} - - -def get_wine_prefix(app_name: Optional[str] = None) -> str: - if app_name is None: - prefix = "~/.wine" - prefix = _config.get("default.env", "WINEPREFIX", fallback=prefix) - prefix = _config.get("default", "wine_prefix", fallback=prefix) - else: - prefix = get_wine_prefix() - prefix = _config.get(f'{app_name}.env', 'WINEPREFIX', fallback=prefix) - prefix = _config.get(app_name, 'wine_prefix', fallback=prefix) - return os.path.abspath(os.path.expanduser(prefix)) From 1a7f89687b71aedc298380929325687c3dfac0e1 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 15 Sep 2023 00:43:34 +0300 Subject: [PATCH 7/9] ConfigHelper: Fix an issue with wine prefix resolution This probably should be split into separate wine and proton discovery functions. --- rare/utils/config_helper.py | 5 +++-- rare/utils/proton.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py index 909d66c4..31ef5973 100644 --- a/rare/utils/config_helper.py +++ b/rare/utils/config_helper.py @@ -64,11 +64,12 @@ def get_game_envvar(option: str, app_name: Optional[str] = None, fallback: Any = def get_wine_prefix(app_name: Optional[str] = None, fallback: Any = None) -> str: _prefix = os.path.join( _config.get("default.env", "STEAM_COMPAT_DATA_PATH", fallback=fallback), "pfx") + if app_name is not None: + _prefix = os.path.join( + _config.get(f'{app_name}.env', "STEAM_COMPAT_DATA_PATH", fallback=_prefix), "pfx") _prefix = _config.get("default.env", "WINEPREFIX", fallback=_prefix) _prefix = _config.get("default", "wine_prefix", fallback=_prefix) if app_name is not None: - _prefix = os.path.join( - _config.get(f'{app_name}.env', "STEAM_COMPAT_DATA_PATH", fallback=fallback), "pfx") _prefix = _config.get(f'{app_name}.env', 'WINEPREFIX', fallback=_prefix) _prefix = _config.get(app_name, 'wine_prefix', fallback=_prefix) return _prefix diff --git a/rare/utils/proton.py b/rare/utils/proton.py index aa4ec7c3..c0e3fb60 100644 --- a/rare/utils/proton.py +++ b/rare/utils/proton.py @@ -216,9 +216,9 @@ def get_steam_environment(tool: Optional[Union[ProtonTool, CompatibilityTool]], def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: steam_path = find_steam() - logger.info("Using Steam install in %s", steam_path) + logger.debug("Using Steam install in %s", steam_path) steam_libraries = find_libraries(steam_path) - logger.info("Searching for tools in libraries %s", steam_libraries) + logger.debug("Searching for tools in libraries %s", steam_libraries) runtimes = {} for library in steam_libraries: From 870aeea7487d722f8e04eec9e012a65ac63aa0c8 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Mon, 4 Sep 2023 00:29:15 +0300 Subject: [PATCH 8/9] EOSGroup: Use UI membership instead of double inheritance --- .../tabs/games/integrations/eos_group.py | 102 +++++++++--------- .../tabs/games/integrations/eos_widget.py | 90 +++++++--------- .../tabs/games/integrations/eos_widget.ui | 60 +++++------ 3 files changed, 115 insertions(+), 137 deletions(-) diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py index 174f34aa..9e541d25 100644 --- a/rare/components/tabs/games/integrations/eos_group.py +++ b/rare/components/tabs/games/integrations/eos_group.py @@ -10,8 +10,9 @@ from legendary.lfs import eos from rare.models.install import InstallOptionsModel from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton from rare.ui.components.tabs.games.integrations.eos_widget import Ui_EosWidget +from rare.utils.misc import icon -logger = getLogger("EOS") +logger = getLogger("EpicOverlay") def get_wine_prefixes() -> List[str]: @@ -42,83 +43,86 @@ class CheckForUpdateWorker(QRunnable): self.signals.update_available.emit(self.core.overlay_update_available) -class EOSGroup(QGroupBox, Ui_EosWidget): +class EOSGroup(QGroupBox): def __init__(self, parent=None): super(EOSGroup, self).__init__(parent=parent) - self.setupUi(self) + self.ui = Ui_EosWidget() + self.ui.setupUi(self) # lk: set object names for CSS properties - self.install_button.setObjectName("InstallButton") - self.uninstall_button.setObjectName("UninstallButton") + self.ui.install_button.setObjectName("InstallButton") + self.ui.install_button.setIcon(icon("ri.install-line")) + self.ui.uninstall_button.setObjectName("UninstallButton") + self.ui.uninstall_button.setIcon(icon("ri.uninstall-line")) self.core = LegendaryCoreSingleton() self.signals = GlobalSignalsSingleton() self.prefix_enabled = False - self.enabled_cb.stateChanged.connect(self.change_enable) - self.uninstall_button.clicked.connect(self.uninstall_overlay) + self.ui.enabled_cb.stateChanged.connect(self.change_enable) + self.ui.uninstall_button.clicked.connect(self.uninstall_overlay) - self.update_button.setVisible(False) + self.ui.update_button.setVisible(False) self.overlay = self.core.lgd.get_overlay_install_info() self.signals.application.overlay_installed.connect(self.overlay_installation_finished) self.signals.application.prefix_updated.connect(self.update_prefixes) - self.update_check_button.clicked.connect(self.check_for_update) - self.install_button.clicked.connect(self.install_overlay) - self.update_button.clicked.connect(lambda: self.install_overlay(True)) + self.ui.update_check_button.clicked.connect(self.check_for_update) + self.ui.install_button.clicked.connect(self.install_overlay) + self.ui.update_button.clicked.connect(lambda: self.install_overlay(True)) if self.overlay: # installed - self.installed_version_lbl.setText(f"{self.overlay.version}") - self.installed_path_lbl.setText(f"{self.overlay.install_path}") - self.overlay_stack.setCurrentIndex(0) + self.ui.installed_version_lbl.setText(f"{self.overlay.version}") + self.ui.installed_path_lbl.setText(f"{self.overlay.install_path}") + self.ui.overlay_stack.setCurrentIndex(0) else: - self.overlay_stack.setCurrentIndex(1) - self.enable_frame.setDisabled(True) + self.ui.overlay_stack.setCurrentIndex(1) + self.ui.enable_frame.setDisabled(True) if platform.system() == "Windows": self.current_prefix = None - self.select_pfx_combo.setVisible(False) + self.ui.select_pfx_combo.setVisible(False) else: self.current_prefix = os.path.expanduser("~/.wine") \ if os.path.exists(os.path.expanduser("~/.wine")) \ else None pfxs = get_wine_prefixes() for pfx in pfxs: - self.select_pfx_combo.addItem(pfx.replace(os.path.expanduser("~/"), "~/")) + self.ui.select_pfx_combo.addItem(pfx.replace(os.path.expanduser("~/"), "~/")) if not pfxs: - self.enable_frame.setDisabled(True) + self.ui.enable_frame.setDisabled(True) else: - self.select_pfx_combo.setCurrentIndex(0) + self.ui.select_pfx_combo.setCurrentIndex(0) - self.select_pfx_combo.currentIndexChanged.connect(self.update_select_combo) + self.ui.select_pfx_combo.currentIndexChanged.connect(self.update_select_combo) if pfxs: self.update_select_combo(None) - self.enabled_info_label.setText("") + self.ui.enabled_info_label.setText("") self.threadpool = QThreadPool.globalInstance() def update_prefixes(self): logger.debug("Updated prefixes") pfxs = get_wine_prefixes() # returns /home/whatever - self.select_pfx_combo.clear() + self.ui.select_pfx_combo.clear() for pfx in pfxs: - self.select_pfx_combo.addItem(pfx.replace(os.path.expanduser("~/"), "~/")) + self.ui.select_pfx_combo.addItem(pfx.replace(os.path.expanduser("~/"), "~/")) if self.current_prefix in pfxs: - self.select_pfx_combo.setCurrentIndex( - self.select_pfx_combo.findText(self.current_prefix.replace(os.path.expanduser("~/"), "~/"))) + self.ui.select_pfx_combo.setCurrentIndex( + self.ui.select_pfx_combo.findText(self.current_prefix.replace(os.path.expanduser("~/"), "~/"))) def check_for_update(self): def worker_finished(update_available): - self.update_button.setVisible(update_available) - self.update_check_button.setDisabled(False) + self.ui.update_button.setVisible(update_available) + self.ui.update_check_button.setDisabled(False) if not update_available: - self.update_check_button.setText(self.tr("No update available")) + self.ui.update_check_button.setText(self.tr("No update available")) - self.update_check_button.setDisabled(True) + self.ui.update_check_button.setDisabled(True) worker = CheckForUpdateWorker() worker.signals.update_available.connect(worker_finished) QThreadPool.globalInstance().start(worker) @@ -131,18 +135,18 @@ class EOSGroup(QGroupBox, Ui_EosWidget): QMessageBox.warning(self, "Error", self.tr("Something went wrong, when installing overlay")) return - self.overlay_stack.setCurrentIndex(0) - self.installed_version_lbl.setText(f"{self.overlay.version}") - self.installed_path_lbl.setText(f"{self.overlay.install_path}") + self.ui.overlay_stack.setCurrentIndex(0) + self.ui.installed_version_lbl.setText(f"{self.overlay.version}") + self.ui.installed_path_lbl.setText(f"{self.overlay.install_path}") - self.update_button.setVisible(False) + self.ui.update_button.setVisible(False) - self.enable_frame.setEnabled(True) + self.ui.enable_frame.setEnabled(True) def update_select_combo(self, i: None): if i is None: - i = self.select_pfx_combo.currentIndex() - prefix = os.path.expanduser(self.select_pfx_combo.itemText(i)) + i = self.ui.select_pfx_combo.currentIndex() + prefix = os.path.expanduser(self.ui.select_pfx_combo.itemText(i)) if platform.system() != "Windows" and not os.path.isfile(os.path.join(prefix, "user.reg")): return self.current_prefix = prefix @@ -151,10 +155,10 @@ class EOSGroup(QGroupBox, Ui_EosWidget): overlay_enabled = False if reg_paths['overlay_path'] and self.core.is_overlay_install(reg_paths['overlay_path']): overlay_enabled = True - self.enabled_cb.setChecked(overlay_enabled) + self.ui.enabled_cb.setChecked(overlay_enabled) def change_enable(self): - enabled = self.enabled_cb.isChecked() + enabled = self.ui.enabled_cb.isChecked() if not enabled: try: eos.remove_registry_entries(self.current_prefix) @@ -164,7 +168,7 @@ class EOSGroup(QGroupBox, Ui_EosWidget): "Failed to disable Overlay. Probably it is installed by Epic Games Launcher")) return logger.info("Disabled Epic Overlay") - self.enabled_info_label.setText(self.tr("Disabled")) + self.ui.enabled_info_label.setText(self.tr("Disabled")) else: if not self.overlay: available_installs = self.core.search_overlay_installs(self.current_prefix) @@ -177,7 +181,7 @@ class EOSGroup(QGroupBox, Ui_EosWidget): if not self.core.is_overlay_install(path): logger.error(f'Not a valid Overlay installation: {path}') - self.select_pfx_combo.removeItem(self.select_pfx_combo.currentIndex()) + self.ui.select_pfx_combo.removeItem(self.ui.select_pfx_combo.currentIndex()) return path = os.path.normpath(path) @@ -202,7 +206,7 @@ class EOSGroup(QGroupBox, Ui_EosWidget): QMessageBox.warning(self, "Error", self.tr( "Failed to enable EOS overlay. Maybe it is already installed by Epic Games Launcher")) return - self.enabled_info_label.setText(self.tr("Enabled")) + self.ui.enabled_info_label.setText(self.tr("Enabled")) logger.info(f'Enabled overlay at: {path}') def update_checkbox(self): @@ -210,14 +214,14 @@ class EOSGroup(QGroupBox, Ui_EosWidget): enabled = False if reg_paths['overlay_path'] and self.core.is_overlay_install(reg_paths['overlay_path']): enabled = True - self.enabled_cb.setChecked(enabled) + self.ui.enabled_cb.setChecked(enabled) def install_overlay(self, update=False): base_path = os.path.join(self.core.get_default_install_dir(), ".overlay") if update: if not self.overlay: - self.overlay_stack.setCurrentIndex(1) - self.enable_frame.setDisabled(True) + self.ui.overlay_stack.setCurrentIndex(1) + self.ui.enable_frame.setDisabled(True) QMessageBox.warning(self, "Warning", self.tr("Overlay is not installed. Could not update")) return base_path = self.overlay.install_path @@ -231,7 +235,7 @@ class EOSGroup(QGroupBox, Ui_EosWidget): def uninstall_overlay(self): if not self.core.is_overlay_installed(): logger.error('No legendary-managed overlay installation found.') - self.overlay_stack.setCurrentIndex(1) + self.ui.overlay_stack.setCurrentIndex(1) return if QMessageBox.No == QMessageBox.question( @@ -242,7 +246,7 @@ class EOSGroup(QGroupBox, Ui_EosWidget): if platform.system() == "Windows": eos.remove_registry_entries(None) else: - for prefix in [self.select_pfx_combo.itemText(i) for i in range(self.select_pfx_combo.count())]: + for prefix in [self.ui.select_pfx_combo.itemText(i) for i in range(self.ui.select_pfx_combo.count())]: logger.info(f"Removing registry entries from {prefix}") try: eos.remove_registry_entries(os.path.expanduser(prefix)) @@ -250,6 +254,6 @@ class EOSGroup(QGroupBox, Ui_EosWidget): logger.warning(f"{prefix}: {e}") self.core.remove_overlay_install() - self.overlay_stack.setCurrentIndex(1) + self.ui.overlay_stack.setCurrentIndex(1) - self.enable_frame.setDisabled(True) + self.ui.enable_frame.setDisabled(True) diff --git a/rare/ui/components/tabs/games/integrations/eos_widget.py b/rare/ui/components/tabs/games/integrations/eos_widget.py index a6ecce82..8b856256 100644 --- a/rare/ui/components/tabs/games/integrations/eos_widget.py +++ b/rare/ui/components/tabs/games/integrations/eos_widget.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/import_sync/eos_widget.ui' +# Form implementation generated from reading ui file 'rare/ui/components/tabs/games/integrations/eos_widget.ui' # -# Created by: PyQt5 UI code generator 5.15.7 +# Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -30,99 +30,85 @@ class Ui_EosWidget(object): self.overlay_stack.setObjectName("overlay_stack") self.overlay_info_page = QtWidgets.QWidget() self.overlay_info_page.setObjectName("overlay_info_page") - self.formLayout_3 = QtWidgets.QFormLayout(self.overlay_info_page) - self.formLayout_3.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.formLayout_3.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.formLayout_3.setObjectName("formLayout_3") + self.overlay_info_layout = QtWidgets.QFormLayout(self.overlay_info_page) + self.overlay_info_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.overlay_info_layout.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) + self.overlay_info_layout.setObjectName("overlay_info_layout") self.installed_version_info_lbl = QtWidgets.QLabel(self.overlay_info_page) self.installed_version_info_lbl.setObjectName("installed_version_info_lbl") - self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.installed_version_info_lbl) + self.overlay_info_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.installed_version_info_lbl) self.installed_version_lbl = QtWidgets.QLabel(self.overlay_info_page) self.installed_version_lbl.setText("error") self.installed_version_lbl.setObjectName("installed_version_lbl") - self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.installed_version_lbl) + self.overlay_info_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.installed_version_lbl) self.installed_path_info_lbl = QtWidgets.QLabel(self.overlay_info_page) self.installed_path_info_lbl.setObjectName("installed_path_info_lbl") - self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.installed_path_info_lbl) + self.overlay_info_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.installed_path_info_lbl) self.installed_path_lbl = QtWidgets.QLabel(self.overlay_info_page) self.installed_path_lbl.setText("error") self.installed_path_lbl.setObjectName("installed_path_lbl") - self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.installed_path_lbl) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") + self.overlay_info_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.installed_path_lbl) + self.info_buttons_layout = QtWidgets.QHBoxLayout() + self.info_buttons_layout.setObjectName("info_buttons_layout") spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) + self.info_buttons_layout.addItem(spacerItem) self.uninstall_button = QtWidgets.QPushButton(self.overlay_info_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.uninstall_button.sizePolicy().hasHeightForWidth()) - self.uninstall_button.setSizePolicy(sizePolicy) - self.uninstall_button.setMaximumSize(QtCore.QSize(150, 16777215)) + self.uninstall_button.setMinimumSize(QtCore.QSize(120, 0)) self.uninstall_button.setObjectName("uninstall_button") - self.horizontalLayout.addWidget(self.uninstall_button) + self.info_buttons_layout.addWidget(self.uninstall_button) self.update_check_button = QtWidgets.QPushButton(self.overlay_info_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.update_check_button.sizePolicy().hasHeightForWidth()) - self.update_check_button.setSizePolicy(sizePolicy) - self.update_check_button.setMaximumSize(QtCore.QSize(150, 16777215)) + self.update_check_button.setMinimumSize(QtCore.QSize(120, 0)) self.update_check_button.setObjectName("update_check_button") - self.horizontalLayout.addWidget(self.update_check_button) + self.info_buttons_layout.addWidget(self.update_check_button) self.update_button = QtWidgets.QPushButton(self.overlay_info_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.update_button.sizePolicy().hasHeightForWidth()) - self.update_button.setSizePolicy(sizePolicy) - self.update_button.setMaximumSize(QtCore.QSize(150, 16777215)) + self.update_button.setMinimumSize(QtCore.QSize(120, 0)) self.update_button.setObjectName("update_button") - self.horizontalLayout.addWidget(self.update_button) - self.formLayout_3.setLayout(3, QtWidgets.QFormLayout.SpanningRole, self.horizontalLayout) + self.info_buttons_layout.addWidget(self.update_button) + self.overlay_info_layout.setLayout(3, QtWidgets.QFormLayout.SpanningRole, self.info_buttons_layout) spacerItem1 = QtWidgets.QSpacerItem(6, 6, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.formLayout_3.setItem(2, QtWidgets.QFormLayout.SpanningRole, spacerItem1) + self.overlay_info_layout.setItem(2, QtWidgets.QFormLayout.SpanningRole, spacerItem1) self.overlay_stack.addWidget(self.overlay_info_page) self.overlay_install_page = QtWidgets.QWidget() self.overlay_install_page.setObjectName("overlay_install_page") - self.formLayout = QtWidgets.QFormLayout(self.overlay_install_page) - self.formLayout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.formLayout.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.formLayout.setObjectName("formLayout") + self.overlay_install_layout = QtWidgets.QFormLayout(self.overlay_install_page) + self.overlay_install_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.overlay_install_layout.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) + self.overlay_install_layout.setObjectName("overlay_install_layout") self.label = QtWidgets.QLabel(self.overlay_install_page) self.label.setObjectName("label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.label) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.overlay_install_layout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.label) + self.install_buttons_layout = QtWidgets.QHBoxLayout() + self.install_buttons_layout.setObjectName("install_buttons_layout") spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_3.addItem(spacerItem2) + self.install_buttons_layout.addItem(spacerItem2) self.install_button = QtWidgets.QPushButton(self.overlay_install_page) + self.install_button.setMinimumSize(QtCore.QSize(120, 0)) self.install_button.setObjectName("install_button") - self.horizontalLayout_3.addWidget(self.install_button) - self.formLayout.setLayout(2, QtWidgets.QFormLayout.SpanningRole, self.horizontalLayout_3) + self.install_buttons_layout.addWidget(self.install_button) + self.overlay_install_layout.setLayout(2, QtWidgets.QFormLayout.SpanningRole, self.install_buttons_layout) spacerItem3 = QtWidgets.QSpacerItem(6, 6, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.formLayout.setItem(1, QtWidgets.QFormLayout.SpanningRole, spacerItem3) + self.overlay_install_layout.setItem(1, QtWidgets.QFormLayout.SpanningRole, spacerItem3) self.overlay_stack.addWidget(self.overlay_install_page) self.eos_layout.addWidget(self.overlay_stack) self.enable_frame = QtWidgets.QFrame(EosWidget) self.enable_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) self.enable_frame.setFrameShadow(QtWidgets.QFrame.Raised) self.enable_frame.setObjectName("enable_frame") - self.verticalLayout = QtWidgets.QVBoxLayout(self.enable_frame) - self.verticalLayout.setObjectName("verticalLayout") + self.enable_layout = QtWidgets.QVBoxLayout(self.enable_frame) + self.enable_layout.setObjectName("enable_layout") self.select_pfx_combo = QtWidgets.QComboBox(self.enable_frame) self.select_pfx_combo.setObjectName("select_pfx_combo") - self.verticalLayout.addWidget(self.select_pfx_combo) + self.enable_layout.addWidget(self.select_pfx_combo) self.enabled_cb = QtWidgets.QCheckBox(self.enable_frame) self.enabled_cb.setObjectName("enabled_cb") - self.verticalLayout.addWidget(self.enabled_cb) + self.enable_layout.addWidget(self.enabled_cb) self.enabled_info_label = QtWidgets.QLabel(self.enable_frame) font = QtGui.QFont() font.setItalic(True) self.enabled_info_label.setFont(font) self.enabled_info_label.setText("") self.enabled_info_label.setObjectName("enabled_info_label") - self.verticalLayout.addWidget(self.enabled_info_label) + self.enable_layout.addWidget(self.enabled_info_label) self.eos_layout.addWidget(self.enable_frame) self.retranslateUi(EosWidget) diff --git a/rare/ui/components/tabs/games/integrations/eos_widget.ui b/rare/ui/components/tabs/games/integrations/eos_widget.ui index 7a70d318..84e017bf 100644 --- a/rare/ui/components/tabs/games/integrations/eos_widget.ui +++ b/rare/ui/components/tabs/games/integrations/eos_widget.ui @@ -38,7 +38,7 @@ 0 - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -74,9 +74,9 @@ - + - + Qt::Horizontal @@ -90,16 +90,10 @@ - - - 0 - 0 - - - + - 150 - 16777215 + 120 + 0 @@ -109,16 +103,10 @@ - - - 0 - 0 - - - + - 150 - 16777215 + 120 + 0 @@ -128,16 +116,10 @@ - - - 0 - 0 - - - + - 150 - 16777215 + 120 + 0 @@ -148,7 +130,7 @@ - + Qt::Vertical @@ -163,7 +145,7 @@ - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -178,9 +160,9 @@ - + - + Qt::Horizontal @@ -194,6 +176,12 @@ + + + 120 + 0 + + Install @@ -202,7 +190,7 @@ - + Qt::Vertical @@ -226,7 +214,7 @@ QFrame::Raised - + From 600441a614dd9b2d3a2cfc87f910b323b30d13fe Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 23 Dec 2023 21:37:44 +0200 Subject: [PATCH 9/9] Rare: Import platform specific modules only on said platforms --- .../components/tabs/settings/game_settings.py | 26 +++++++++++-------- .../tabs/settings/widgets/env_vars_model.py | 10 +++++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/rare/components/tabs/settings/game_settings.py b/rare/components/tabs/settings/game_settings.py index d52dd79f..6b96d5f6 100644 --- a/rare/components/tabs/settings/game_settings.py +++ b/rare/components/tabs/settings/game_settings.py @@ -8,12 +8,15 @@ from PyQt5.QtWidgets import ( ) from rare.components.tabs.settings.widgets.env_vars import EnvVars -from rare.components.tabs.settings.widgets.linux import LinuxSettings -from rare.components.tabs.settings.widgets.proton import ProtonSettings from rare.components.tabs.settings.widgets.wrapper import WrapperSettings from rare.shared import LegendaryCoreSingleton from rare.ui.components.tabs.settings.game_settings import Ui_GameSettings +if platform.system() != "Windows": + from rare.components.tabs.settings.widgets.linux import LinuxSettings + if platform.system() != "Darwin": + from rare.components.tabs.settings.widgets.proton import ProtonSettings + logger = getLogger("GameSettings") @@ -88,15 +91,16 @@ class DefaultGameSettings(QWidget): self.env_vars.update_game(app_name) -class LinuxAppSettings(LinuxSettings): - def __init__(self): - super(LinuxAppSettings, self).__init__() +if platform.system() != "Windows": + class LinuxAppSettings(LinuxSettings): + def __init__(self): + super(LinuxAppSettings, self).__init__() - def update_game(self, app_name): - self.name = app_name - self.wine_prefix.setText(self.load_prefix()) - self.wine_exec.setText(self.load_setting(self.name, "wine_executable")) + def update_game(self, app_name): + self.name = app_name + self.wine_prefix.setText(self.load_prefix()) + self.wine_exec.setText(self.load_setting(self.name, "wine_executable")) - self.dxvk.load_settings(self.name) + self.dxvk.load_settings(self.name) - self.mangohud.load_settings(self.name) + self.mangohud.load_settings(self.name) diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/env_vars_model.py index 795533e7..d8f5d8c1 100644 --- a/rare/components/tabs/settings/widgets/env_vars_model.py +++ b/rare/components/tabs/settings/widgets/env_vars_model.py @@ -1,3 +1,4 @@ +import platform import re import sys from collections import ChainMap @@ -8,7 +9,10 @@ from PyQt5.QtGui import QFont from rare.lgndr.core import LegendaryCore from rare.utils.misc import icon -from rare.utils import proton + +if platform.system() != "Windows": + if platform.system() != "Darwin": + from rare.utils import proton class EnvVarsTableModel(QAbstractTableModel): @@ -28,7 +32,9 @@ class EnvVarsTableModel(QAbstractTableModel): "DXVK_HUD", "MANGOHUD_CONFIG", ] - self.__readonly.extend(proton.get_steam_environment(None).keys()) + if platform.system() != "Windows": + if platform.system() != "Darwin": + self.__readonly.extend(proton.get_steam_environment(None).keys()) self.__default: str = "default" self.__appname: str = None