1
0
Fork 0
mirror of synced 2024-05-18 11:32:50 +12:00
Rare/rare/components/tabs/games/integrations/egl_sync_group.py
loathingKernel 5bf353ec37
Rare: Replace QToolButton with QPushButton
QToolButton is not really designed to be used in the way we did and since
QPushButton supports having a menu attached to, we can replace tool buttons
in most cases.

* Fix the presentation of the TabButtonWidget by updating RareStyle's css

* Reduce the size of the top tab bar to save vertical space.

* Remove infoLabel property
2024-02-21 20:25:03 +02:00

398 lines
15 KiB
Python

import os
import platform
from abc import abstractmethod
from logging import getLogger
from typing import Tuple, Iterable, List, Union
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox, QFrame, QFormLayout
from legendary.models.egl import EGLManifest
from legendary.models.game import InstalledGame
from rare.lgndr.glue.exception import LgndrException
from rare.models.pathspec import PathSpec
from rare.shared import RareCore
from rare.shared.workers.wine_resolver import WinePathResolver
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
from rare.utils.compat import utils as compat_utils
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
logger = getLogger("EGLSync")
class EGLSyncGroup(QGroupBox):
def __init__(self, parent=None):
super(EGLSyncGroup, self).__init__(parent=parent)
self.ui = Ui_EGLSyncGroup()
self.ui.setupUi(self)
self.core = RareCore.instance().core()
self.egl_path_edit = PathEdit(
path=self.core.egl.programdata_path,
placeholder=self.tr(
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
),
file_mode=QFileDialog.DirectoryOnly,
edit_func=self.egl_path_edit_edit_cb,
save_func=self.egl_path_edit_save_cb,
parent=self,
)
self.ui.egl_sync_layout.setWidget(
self.ui.egl_sync_layout.getWidgetPosition(self.ui.egl_path_edit_label)[0],
QFormLayout.FieldRole, self.egl_path_edit
)
self.egl_path_info = ElideLabel(parent=self)
self.ui.egl_sync_layout.setWidget(
self.ui.egl_sync_layout.getWidgetPosition(self.ui.egl_path_info_label)[0],
QFormLayout.FieldRole, self.egl_path_info
)
if platform.system() == "Windows":
self.ui.egl_path_edit_label.setEnabled(False)
self.egl_path_edit.setEnabled(False)
self.ui.egl_path_info_label.setEnabled(False)
self.egl_path_info.setEnabled(False)
else:
self.egl_path_edit.textChanged.connect(self.egl_path_changed)
if self.core.egl.programdata_path:
self.ui.egl_path_info_label.setEnabled(True)
self.egl_path_info.setEnabled(True)
self.ui.egl_sync_check.setChecked(self.core.egl_sync_enabled)
self.ui.egl_sync_check.stateChanged.connect(self.egl_sync_changed)
# lk: Temporarily disable automatic sync with EGL
self.ui.egl_sync_check_label.setHidden(True)
self.ui.egl_sync_check.setHidden(True)
self.import_list = EGLSyncImportGroup(parent=self)
self.ui.import_export_layout.addWidget(self.import_list)
self.export_list = EGLSyncExportGroup(parent=self)
self.ui.import_export_layout.addWidget(self.export_list)
# self.egl_watcher = QFileSystemWatcher([self.egl_path_edit.text()], self)
# self.egl_watcher.directoryChanged.connect(self.update_lists)
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if not self.core.egl.programdata_path:
self.__run_wine_resolver()
self.update_lists()
super().showEvent(a0)
def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WinePathResolver(self.core, "default", str(PathSpec.egl_programdata()))
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(wine_resolver)
def __on_wine_resolver_result(self, path):
self.egl_path_info.setText(path)
if not path:
self.egl_path_info.setText(
self.tr(
"Default Wine prefix is unset, or path does not exist. "
"Create it or configure it in Settings -> Linux."
)
)
elif not os.path.exists(path):
self.egl_path_info.setText(
self.tr(
"Default Wine prefix is set but EGL manifests path does not exist. "
"Your configured default Wine prefix might not be where EGL is installed."
)
)
else:
self.egl_path_edit.setText(path)
@staticmethod
def egl_path_edit_edit_cb(path) -> Tuple[bool, str, int]:
if not path:
return True, path, IndicatorReasonsCommon.VALID
if os.path.exists(os.path.join(path, "system.reg")) and os.path.exists(
os.path.join(path, "dosdevices/c:")
):
# path is a wine prefix
path = PathSpec.prefix_egl_programdata(path)
elif not path.rstrip("/").endswith(PathSpec.wine_egl_programdata()):
# lower() might or might not be needed in the check
return False, path, IndicatorReasonsCommon.WRONG_FORMAT
if os.path.exists(path):
return True, path, IndicatorReasonsCommon.VALID
return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS
def egl_path_edit_save_cb(self, path):
if not path or not os.path.exists(path):
# This is the same as "--unlink"
self.core.egl.programdata_path = None
self.core.lgd.config.remove_option("Legendary", "egl_programdata")
self.core.lgd.config.remove_option("Legendary", "egl_sync")
# remove EGL GUIDs from all games, DO NOT remove .egstore folders because that would fuck things up.
for igame in self.core.get_installed_list():
igame.egl_guid = ""
self.core.install_game(igame)
else:
self.core.egl.programdata_path = path
self.core.lgd.config.set("Legendary", "egl_programdata", path)
self.core.lgd.save_config()
def egl_path_changed(self, path):
if self.egl_path_edit.is_valid:
self.ui.egl_sync_check.setEnabled(bool(path))
self.ui.egl_sync_check.setCheckState(Qt.Unchecked)
# self.egl_watcher.removePaths([p for p in self.egl_watcher.directories()])
# self.egl_watcher.addPaths([path])
self.update_lists()
def egl_sync_changed(self, state):
if state == Qt.Unchecked:
self.import_list.setEnabled(bool(self.import_list.items))
self.export_list.setEnabled(bool(self.export_list.items))
self.core.lgd.config.remove_option("Legendary", "egl_sync")
else:
self.core.lgd.config.set("Legendary", "egl_sync", str(True))
# lk: do import/export here since automatic sync was selected
self.import_list.mark(Qt.Checked)
self.export_list.mark(Qt.Checked)
sync_worker = EGLSyncWorker(self.import_list, self.export_list)
QThreadPool.globalInstance().start(sync_worker)
self.import_list.setEnabled(False)
self.export_list.setEnabled(False)
# self.update_lists()
self.core.lgd.save_config()
def update_lists(self):
# self.egl_watcher.blockSignals(True)
have_path = False
if self.core.egl.programdata_path:
have_path = os.path.exists(self.core.egl.programdata_path)
if not have_path and os.path.isdir(os.path.dirname(self.core.egl.programdata_path)):
# NOTE: This might happen if EGL is installed but no games have been installed through it
os.mkdir(self.core.egl.programdata_path)
have_path = os.path.isdir(self.core.egl.programdata_path)
# NOTE: need to clear known manifests to force refresh
self.core.egl.manifests.clear()
self.ui.egl_sync_check_label.setEnabled(have_path)
self.ui.egl_sync_check.setEnabled(have_path)
self.import_list.populate(have_path)
self.import_list.setEnabled(have_path)
self.export_list.populate(have_path)
self.export_list.setEnabled(have_path)
# self.egl_watcher.blockSignals(False)
class EGLSyncListItem(QListWidgetItem):
def __init__(self, game: Union[EGLManifest,InstalledGame], parent=None):
super(EGLSyncListItem, self).__init__(parent=parent)
self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
self.setCheckState(Qt.Unchecked)
self.core = RareCore.instance().core()
self.game = game
self.setText(self.app_title)
def is_checked(self) -> bool:
return self.checkState() == Qt.Checked
@abstractmethod
def action(self) -> Union[str, bool]:
pass
@property
def app_name(self):
return self.game.app_name
@property
@abstractmethod
def app_title(self) -> str:
pass
class EGLSyncExportItem(EGLSyncListItem):
def __init__(self, game: InstalledGame, parent=None):
super(EGLSyncExportItem, self).__init__(game=game, parent=parent)
def action(self) -> Union[str,bool]:
error = False
try:
self.core.egl_export(self.game.app_name)
except LgndrException as ret:
error = ret.message
return error
@property
def app_title(self) -> str:
return self.game.title
class EGLSyncImportItem(EGLSyncListItem):
def __init__(self, game: EGLManifest, parent=None):
super(EGLSyncImportItem, self).__init__(game=game, parent=parent)
def action(self) -> Union[str,bool]:
error = False
try:
self.core.egl_import(self.game.app_name)
except LgndrException as ret:
error = ret.message
return error
@property
def app_title(self) -> str:
return self.core.get_game(self.game.app_name).app_title
class EGLSyncListGroup(QGroupBox):
action_errors = pyqtSignal(list)
def __init__(self, parent=None):
super(EGLSyncListGroup, self).__init__(parent=parent)
self.ui = Ui_EGLSyncListGroup()
self.ui.setupUi(self)
self.ui.list.setFrameShape(QFrame.NoFrame)
self.rcore = RareCore.instance()
self.core = RareCore.instance().core()
self.ui.list.itemDoubleClicked.connect(
lambda item: item.setCheckState(Qt.Unchecked)
if item.checkState() != Qt.Unchecked
else item.setCheckState(Qt.Checked)
)
self.ui.list.itemChanged.connect(self.has_selected)
self.ui.select_all_button.clicked.connect(lambda: self.mark(Qt.Checked))
self.ui.select_none_button.clicked.connect(lambda: self.mark(Qt.Unchecked))
self.ui.action_button.clicked.connect(self.action)
self.action_errors.connect(self.show_errors)
def has_selected(self):
for item in self.items:
if item.is_checked():
self.ui.action_button.setEnabled(True)
return
self.ui.action_button.setEnabled(False)
def mark(self, state):
for item in self.items:
item.setCheckState(state)
def populate(self, enabled: bool):
self.ui.label.setVisible(not enabled or not bool(self.ui.list.count()))
self.ui.list.setVisible(enabled and bool(self.ui.list.count()))
self.ui.buttons_widget.setVisible(enabled and bool(self.ui.list.count()))
@abstractmethod
def action(self):
pass
@pyqtSlot(list)
@abstractmethod
def show_errors(self, errors: List):
pass
@property
def items(self) -> Iterable[EGLSyncListItem]:
# for i in range(self.list.count()):
# yield self.list.item(i)
return map(self.ui.list.item, range(self.ui.list.count()))
# return [self.ui.list.item(i) for i in range(self.ui.list.count())]
class EGLSyncExportGroup(EGLSyncListGroup):
def __init__(self, parent=None):
super(EGLSyncExportGroup, self).__init__(parent=parent)
self.setTitle(self.tr("Exportable games"))
self.ui.label.setText(self.tr("No games to export to EGL"))
self.ui.action_button.setText(self.tr("Export"))
def populate(self, enabled: bool):
if enabled:
self.ui.list.clear()
for item in self.core.egl_get_exportable():
try:
i = EGLSyncExportItem(item, self.ui.list)
except AttributeError:
logger.error(f"{item.app_name} does not work. Ignoring")
else:
self.ui.list.addItem(i)
super(EGLSyncExportGroup, self).populate(enabled)
@pyqtSlot(list)
def show_errors(self, errors: List):
QMessageBox.warning(
self.parent(),
self.tr("The following errors occurred while exporting."),
"\n".join(errors),
)
def action(self):
errors: List = []
for item in self.items:
if item.is_checked():
if e := item.action():
errors.append(e)
self.populate(True)
if errors:
self.action_errors.emit(errors)
class EGLSyncImportGroup(EGLSyncListGroup):
def __init__(self, parent=None):
super(EGLSyncImportGroup, self).__init__(parent=parent)
self.setTitle(self.tr("Importable games"))
self.ui.label.setText(self.tr("No games to import from EGL"))
self.ui.action_button.setText(self.tr("Import"))
self.list_func = self.core.egl_get_importable
def populate(self, enabled: bool):
if enabled:
self.ui.list.clear()
for item in self.core.egl_get_importable():
try:
i = EGLSyncImportItem(item, self.ui.list)
except AttributeError:
logger.error(f"{item.app_name} does not work. Ignoring")
else:
self.ui.list.addItem(i)
super(EGLSyncImportGroup, self).populate(enabled)
@pyqtSlot(list)
def show_errors(self, errors: List):
QMessageBox.warning(
self.parent(),
self.tr("The following errors occurred while importing."),
"\n".join(errors),
)
def action(self):
errors: List = []
for item in self.items:
if item.is_checked():
if e := item.action():
errors.append(e)
else:
self.rcore.get_game(item.app_name).set_installed(True)
self.populate(True)
if errors:
self.action_errors.emit(errors)
class EGLSyncWorker(QRunnable):
def __init__(self, import_list: EGLSyncListGroup, export_list: EGLSyncListGroup):
super(EGLSyncWorker, self).__init__()
self.setAutoDelete(True)
self.import_list = import_list
self.export_list = export_list
@pyqtSlot()
def run(self):
self.import_list.action()
self.export_list.action()