1
0
Fork 0
mirror of synced 2024-05-24 14:29:58 +12:00

Merge pull request #352 from loathingKernel/next

Preparation for future features
This commit is contained in:
Stelios Tsampas 2023-12-24 21:31:43 +02:00 committed by GitHub
commit 826d38ca55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 809 additions and 238 deletions

View file

@ -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()

View file

@ -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()

View file

@ -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))

View file

@ -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)

View file

@ -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"<b>{self.overlay.version}</b>")
self.installed_path_lbl.setText(f"<b>{self.overlay.install_path}</b>")
self.overlay_stack.setCurrentIndex(0)
self.ui.installed_version_lbl.setText(f"<b>{self.overlay.version}</b>")
self.ui.installed_path_lbl.setText(f"<b>{self.overlay.install_path}</b>")
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"<b>{self.overlay.version}</b>")
self.installed_path_lbl.setText(f"<b>{self.overlay.install_path}</b>")
self.ui.overlay_stack.setCurrentIndex(0)
self.ui.installed_version_lbl.setText(f"<b>{self.overlay.version}</b>")
self.ui.installed_path_lbl.setText(f"<b>{self.overlay.install_path}</b>")
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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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", "<h2>Launching Rare</h2>"))

View file

@ -29,7 +29,7 @@
</size>
</property>
<property name="windowTitle">
<string>Launching - Rare</string>
<string>Launching</string>
</property>
<layout class="QVBoxLayout" name="launch_layout">
<item>

View file

@ -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", "<h1>Welcome to Rare</h1>"))
self.exit_button.setText(_translate("LoginDialog", "Exit"))
self.back_button.setText(_translate("LoginDialog", "Back"))

View file

@ -11,9 +11,9 @@
</rect>
</property>
<property name="windowTitle">
<string>Login - Rare</string>
<string>Login</string>
</property>
<layout class="QVBoxLayout" name="login_layout" stretch="0,0,0,0,0">
<layout class="QVBoxLayout" name="main_layout" stretch="0,0,0,0,0">
<item>
<spacer name="login_vspacer_top">
<property name="orientation">
@ -58,6 +58,13 @@
</item>
<item>
<layout class="QHBoxLayout" name="button_layout">
<item>
<widget class="QPushButton" name="exit_button">
<property name="text">
<string>Exit</string>
</property>
</widget>
</item>
<item>
<spacer name="button_hspacer">
<property name="orientation">
@ -71,13 +78,6 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="exit_button">
<property name="text">
<string>Exit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="back_button">
<property name="text">

View file

@ -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)

View file

@ -38,7 +38,7 @@
<number>0</number>
</property>
<widget class="QWidget" name="overlay_info_page">
<layout class="QFormLayout" name="formLayout_3">
<layout class="QFormLayout" name="overlay_info_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@ -74,9 +74,9 @@
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="info_buttons_layout">
<item>
<spacer name="horizontalSpacer">
<spacer name="info_buttons_hspacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -90,16 +90,10 @@
</item>
<item>
<widget class="QPushButton" name="uninstall_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="minimumSize">
<size>
<width>150</width>
<height>16777215</height>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
@ -109,16 +103,10 @@
</item>
<item>
<widget class="QPushButton" name="update_check_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="minimumSize">
<size>
<width>150</width>
<height>16777215</height>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
@ -128,16 +116,10 @@
</item>
<item>
<widget class="QPushButton" name="update_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="minimumSize">
<size>
<width>150</width>
<height>16777215</height>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
@ -148,7 +130,7 @@
</layout>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer">
<spacer name="info_page_vspacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
@ -163,7 +145,7 @@
</layout>
</widget>
<widget class="QWidget" name="overlay_install_page">
<layout class="QFormLayout" name="formLayout">
<layout class="QFormLayout" name="overlay_install_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@ -178,9 +160,9 @@
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<layout class="QHBoxLayout" name="install_buttons_layout">
<item>
<spacer name="horizontalSpacer_2">
<spacer name="install_buttons_hspacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -194,6 +176,12 @@
</item>
<item>
<widget class="QPushButton" name="install_button">
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Install</string>
</property>
@ -202,7 +190,7 @@
</layout>
</item>
<item row="1" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<spacer name="install_page_vspacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
@ -226,7 +214,7 @@
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="enable_layout">
<item>
<widget class="QComboBox" name="select_pfx_combo"/>
</item>

View file

@ -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)}

271
rare/utils/proton.py Normal file
View file

@ -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

283
rare/widgets/dialogs.py Normal file
View file

@ -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()

View file

@ -4,4 +4,5 @@ QtAwesome
setuptools
legendary-gl>=0.20.34
orjson
vdf; platform_system != "Windows"
pywin32; platform_system == "Windows"