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/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) 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/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 11e16449..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 @@ -9,6 +10,10 @@ from PyQt5.QtGui import QFont from rare.lgndr.core import LegendaryCore from rare.utils.misc import icon +if platform.system() != "Windows": + if platform.system() != "Darwin": + from rare.utils import proton + class EnvVarsTableModel(QAbstractTableModel): def __init__(self, core: LegendaryCore, parent = None): @@ -23,11 +28,13 @@ class EnvVarsTableModel(QAbstractTableModel): self.__readonly = [ "STEAM_COMPAT_DATA_PATH", - "STEAM_COMPAT_CLIENT_INSTALL_PATH", "WINEPREFIX", "DXVK_HUD", "MANGOHUD_CONFIG", ] + 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 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/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/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 - + diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py index aa0bc9ce..31ef5973 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, Set, Any from legendary.core import LegendaryCore from legendary.models.config import LGDConf @@ -40,6 +41,48 @@ 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_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") + 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 = _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 = [os.path.expanduser(prefix) for prefix in _prefixes] + return {p for p in _prefixes if os.path.isdir(p)} + diff --git a/rare/utils/proton.py b/rare/utils/proton.py new file mode 100644 index 00000000..c0e3fb60 --- /dev/null +++ b/rare/utils/proton.py @@ -0,0 +1,271 @@ +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.debug("Using Steam install in %s", steam_path) + steam_libraries = find_libraries(steam_path) + logger.debug("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 = [] + 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 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() 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"