diff --git a/rare/components/__init__.py b/rare/components/__init__.py
index 684a11331..e120412fe 100644
--- a/rare/components/__init__.py
+++ b/rare/components/__init__.py
@@ -76,14 +76,15 @@ def re_login(self):
@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 d231625b1..e63d7a1b5 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 @@ def __init__(self, parent=None):
| 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 @@ def __init__(self, parent=None):
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 @@ def login(self):
# 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 @@ def __on_progress(self, i: int, m: str):
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 42ba6452a..5c2158e76 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 @@ def __init__(self, parent=None):
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 @@ def __init__(self, core: LegendaryCore, parent=None):
| Qt.WindowCloseButtonHint
| Qt.MSWindowsFixedSizeDialogHint
)
- self.setWindowModality(Qt.WindowModal)
+
self.ui = Ui_LoginDialog()
self.ui.setupUi(self)
@@ -93,13 +93,22 @@ def __init__(self, core: LegendaryCore, parent=None):
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 @@ def next_clicked(self):
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 @@ def login_successful(self):
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 d06cd8fc2..32f5dbd25 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 @@ def setupUi(self, widget: QWidget):
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 174f34aa8..9e541d255 100644
--- a/rare/components/tabs/games/integrations/eos_group.py
+++ b/rare/components/tabs/games/integrations/eos_group.py
@@ -10,8 +10,9 @@
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 @@ def run(self) -> None:
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 @@ def overlay_installation_finished(self):
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 @@ def update_select_combo(self, i: None):
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 @@ def change_enable(self):
"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 @@ def change_enable(self):
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 @@ def change_enable(self):
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 @@ def update_checkbox(self):
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 @@ def install_overlay(self, update=False):
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 @@ def uninstall_overlay(self):
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 @@ def uninstall_overlay(self):
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 d52dd79fb..6b96d5f60 100644
--- a/rare/components/tabs/settings/game_settings.py
+++ b/rare/components/tabs/settings/game_settings.py
@@ -8,12 +8,15 @@
)
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 @@ def load_settings(self, app_name):
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 11e16449b..d8f5d8c17 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 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 @@ def __init__(self, core: LegendaryCore, parent = None):
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 110db4ab4..658aaf872 100644
--- a/rare/components/tabs/settings/widgets/proton.py
+++ b/rare/components/tabs/settings/widgets/proton.py
@@ -9,37 +9,13 @@
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 @@ def __init__(self, linux_settings: LinuxSettings, wrapper_settings: WrapperSetti
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 9c30344ed..0808126fb 100644
--- a/rare/ui/components/dialogs/launch_dialog.py
+++ b/rare/ui/components/dialogs/launch_dialog.py
@@ -36,7 +36,7 @@ def setupUi(self, LaunchDialog):
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 b8a5838ec..ff9f6d34b 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 76eb33108..b158a4203 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 5419629b4..6ca927101 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 a6ecce82d..8b8562561 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 @@ def setupUi(self, EosWidget):
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 7a70d3187..84e017bfb 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 aa0bc9ce7..31ef5973c 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 000000000..c0e3fb60f
--- /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 000000000..a5a97db8f
--- /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 cf445de55..7301263bd 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"