import os import platform as pf import sys from typing import Tuple, List, Union, Optional from PyQt5.QtCore import QObject, QRunnable, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, QThreadPool, QSettings from PyQt5.QtGui import QCloseEvent, QKeyEvent from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox, QLayout, QWidget, QVBoxLayout, QApplication from legendary.lfs.eos import EOSOverlayApp from legendary.models.downloading import ConditionCheckResult from legendary.utils.selective_dl import get_sdl_appname from rare.lgndr.cli import LegendaryCLI from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.arguments import LgndrInstallGameArgs from rare.lgndr.glue.exception import LgndrException from rare.lgndr.glue.monkeys import LgndrIndirectStatus from rare.models.game import RareGame from rare.models.install import InstallDownloadModel, InstallQueueItemModel, InstallOptionsModel from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced from rare.utils import config_helper from rare.utils.extra_widgets import PathEdit from rare.utils.misc import get_size from rare.widgets.collapsible_widget import CollapsibleFrame class InstallDialogAdvanced(CollapsibleFrame): def __init__(self, parent=None): widget = QWidget() title = widget.tr("Advanced options") self.ui = Ui_InstallDialogAdvanced() self.ui.setupUi(widget) super(InstallDialogAdvanced, self).__init__(widget=widget, title=title, parent=parent) class InstallDialog(QDialog): result_ready = pyqtSignal(InstallQueueItemModel) def __init__(self, rgame: RareGame, options: InstallOptionsModel, parent=None,): super(InstallDialog, self).__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) self.ui = Ui_InstallDialog() self.ui.setupUi(self) self.core = LegendaryCoreSingleton() self.rgame = rgame self.options = options self.__download: Optional[InstallDownloadModel] = None self.advanced = InstallDialogAdvanced(parent=self) self.ui.advanced_layout.addWidget(self.advanced) self.selectable = CollapsibleFrame(widget=None, title=self.tr("Optional downloads"), parent=self) self.ui.selectable_layout.addWidget(self.selectable) self.options_changed = False self.worker_running = False self.reject_close = True self.threadpool = QThreadPool(self) self.threadpool.setMaxThreadCount(1) if options.repair_mode: header = self.tr("Repair") if options.repair_and_update: header = self.tr("Repair and update") elif options.update: header = self.tr("Update") else: header = self.tr("Install") self.ui.install_dialog_label.setText(f'

{header} "{self.rgame.app_title}"

') self.setWindowTitle(f'{QApplication.instance().applicationName()} - {header} "{self.rgame.app_title}"') if not self.options.base_path: self.options.base_path = self.core.lgd.config.get( "Legendary", "install_dir", fallback=os.path.expanduser("~/legendary") ) self.install_dir_edit = PathEdit( path=self.options.base_path, file_type=QFileDialog.DirectoryOnly, edit_func=self.option_changed, parent=self, ) self.ui.install_dir_layout.addWidget(self.install_dir_edit) if self.options.update: self.ui.install_dir_label.setEnabled(False) self.install_dir_edit.setEnabled(False) self.ui.shortcut_label.setEnabled(False) self.ui.shortcut_check.setEnabled(False) else: self.ui.shortcut_check.setChecked(QSettings().value("create_shortcut", True, bool)) self.error_box() platforms = ["Windows"] if self.rgame.is_win32: platforms.append("Win32") if self.rgame.is_mac: platforms.append("Mac") self.ui.platform_combo.addItems(platforms) self.ui.platform_combo.currentIndexChanged.connect(lambda: self.option_changed(None)) self.ui.platform_combo.currentIndexChanged.connect(lambda: self.error_box()) self.ui.platform_combo.currentIndexChanged.connect( lambda i: self.error_box( self.tr("Warning"), self.tr("You will not be able to run the game if you select {} as platform").format( self.ui.platform_combo.itemText(i) ), ) if (self.ui.platform_combo.currentText() == "Mac" and pf.system() != "Darwin") else None ) if pf.system() == "Darwin" and "Mac" in platforms: self.ui.platform_combo.setCurrentIndex(platforms.index("Mac")) self.ui.platform_combo.currentTextChanged.connect(self.setup_sdl_list) self.advanced.ui.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0)) self.advanced.ui.max_workers_spin.valueChanged.connect(self.option_changed) self.advanced.ui.max_memory_spin.setValue(self.core.lgd.config.getint("Legendary", "max_memory", fallback=0)) self.advanced.ui.max_memory_spin.valueChanged.connect(self.option_changed) self.advanced.ui.dl_optimizations_check.stateChanged.connect(self.option_changed) self.advanced.ui.force_download_check.stateChanged.connect(self.option_changed) self.advanced.ui.ignore_space_check.stateChanged.connect(self.option_changed) self.advanced.ui.download_only_check.stateChanged.connect( lambda: self.non_reload_option_changed("download_only") ) self.ui.shortcut_check.stateChanged.connect( lambda: self.non_reload_option_changed("shortcut") ) self.selectable_checks: List[TagCheckBox] = [] self.config_tags: Optional[List[str]] = None self.setup_sdl_list("Mac" if pf.system() == "Darwin" and "Mac" in platforms else "Windows") self.ui.install_button.setEnabled(False) if self.options.overlay: self.ui.platform_label.setEnabled(False) self.ui.platform_combo.setEnabled(False) self.advanced.ui.ignore_space_label.setEnabled(False) self.advanced.ui.ignore_space_check.setEnabled(False) self.advanced.ui.download_only_label.setEnabled(False) self.advanced.ui.download_only_check.setEnabled(False) self.ui.shortcut_label.setEnabled(False) self.ui.shortcut_check.setEnabled(False) self.selectable.setEnabled(False) if pf.system() == "Darwin": self.ui.shortcut_check.setDisabled(True) self.ui.shortcut_check.setChecked(False) self.ui.shortcut_check.setToolTip(self.tr("Creating a shortcut is not supported on MacOS")) self.advanced.ui.install_prereqs_label.setEnabled(False) self.advanced.ui.install_prereqs_check.setEnabled(False) self.advanced.ui.install_prereqs_check.stateChanged.connect( lambda: self.non_reload_option_changed("install_prereqs") ) self.non_reload_option_changed("shortcut") self.ui.cancel_button.clicked.connect(self.cancel_clicked) self.ui.verify_button.clicked.connect(self.verify_clicked) self.ui.install_button.clicked.connect(self.install_clicked) self.advanced.ui.install_prereqs_check.setChecked(self.options.install_prereqs) self.ui.install_dialog_layout.setSizeConstraint(QLayout.SetFixedSize) def execute(self): if self.options.silent: self.reject_close = False self.get_download_info() else: self.verify_clicked() self.show() @pyqtSlot(str) def setup_sdl_list(self, platform="Windows"): for cb in self.selectable_checks: cb.disconnect() cb.deleteLater() self.selectable_checks.clear() if config_tags := self.core.lgd.config.get(self.rgame.app_name, 'install_tags', fallback=None): self.config_tags = config_tags.split(",") config_disable_sdl = self.core.lgd.config.getboolean(self.rgame.app_name, 'disable_sdl', fallback=False) sdl_name = get_sdl_appname(self.rgame.app_name) if not config_disable_sdl and sdl_name is not None: # FIXME: this should be updated whenever platform changes sdl_data = self.core.get_sdl_data(sdl_name, platform=platform) if sdl_data: widget = QWidget(self.selectable) layout = QVBoxLayout(widget) layout.setSpacing(0) for tag, info in sdl_data.items(): cb = TagCheckBox(info["name"], info["tags"]) if tag == "__required": cb.setChecked(True) cb.setDisabled(True) if self.config_tags is not None: if all(elem in self.config_tags for elem in info["tags"]): cb.setChecked(True) layout.addWidget(cb) self.selectable_checks.append(cb) for cb in self.selectable_checks: cb.stateChanged.connect(self.option_changed) self.selectable.setWidget(widget) else: self.selectable.setDisabled(True) def get_options(self): self.options.base_path = self.install_dir_edit.text() if not self.options.update else None self.options.max_workers = self.advanced.ui.max_workers_spin.value() self.options.shared_memory = self.advanced.ui.max_memory_spin.value() self.options.order_opt = self.advanced.ui.dl_optimizations_check.isChecked() self.options.force = self.advanced.ui.force_download_check.isChecked() self.options.ignore_space = self.advanced.ui.ignore_space_check.isChecked() self.options.no_install = self.advanced.ui.download_only_check.isChecked() self.options.platform = self.ui.platform_combo.currentText() self.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() self.options.create_shortcut = self.ui.shortcut_check.isChecked() if self.selectable_checks: self.options.install_tag = [""] for cb in self.selectable_checks: if data := cb.isChecked(): # noinspection PyTypeChecker self.options.install_tag.extend(data) def get_download_info(self): self.__download = None info_worker = InstallInfoWorker(self.core, self.options) info_worker.signals.result.connect(self.on_worker_result) info_worker.signals.failed.connect(self.on_worker_failed) info_worker.signals.finished.connect(self.on_worker_finished) self.worker_running = True self.threadpool.start(info_worker) def verify_clicked(self): self.error_box() message = self.tr("Updating...") self.ui.download_size_text.setText(message) self.ui.download_size_text.setStyleSheet("font-style: italic; font-weight: normal") self.ui.install_size_text.setText(message) self.ui.install_size_text.setStyleSheet("font-style: italic; font-weight: normal") self.ui.cancel_button.setEnabled(False) self.ui.verify_button.setEnabled(False) self.ui.install_button.setEnabled(False) self.options_changed = False self.get_options() self.get_download_info() def option_changed(self, path) -> Tuple[bool, str, str]: self.options_changed = True self.ui.install_button.setEnabled(False) self.ui.verify_button.setEnabled(not self.worker_running) return True, path, "" def non_reload_option_changed(self, option: str): if option == "download_only": self.options.no_install = self.advanced.ui.download_only_check.isChecked() elif option == "shortcut": QSettings().setValue("create_shortcut", self.ui.shortcut_check.isChecked()) self.options.create_shortcut = self.ui.shortcut_check.isChecked() elif option == "install_prereqs": self.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() def cancel_clicked(self): if self.config_tags: config_helper.add_option(self.rgame.app_name, 'install_tags', ','.join(self.config_tags)) else: # lk: this is purely for cleaning any install tags we might have added erroneously to the config config_helper.remove_option(self.rgame.app_name, 'install_tags') self.__download = None self.reject_close = False self.close() def install_clicked(self): self.reject_close = False self.close() @pyqtSlot(InstallQueueItemModel) def on_worker_result(self, dl_item: InstallQueueItemModel): self.__download = dl_item.download download_size = dl_item.download.analysis.dl_size install_size = dl_item.download.analysis.install_size # install_size = self.dl_item.download.analysis.disk_space_delta if download_size: self.ui.download_size_text.setText(get_size(download_size)) self.ui.download_size_text.setStyleSheet("font-style: normal; font-weight: bold") self.ui.install_button.setEnabled(not self.options_changed) else: self.ui.install_size_text.setText(self.tr("Game already installed")) self.ui.install_size_text.setStyleSheet("font-style: italics; font-weight: normal") self.ui.install_size_text.setText(get_size(install_size)) self.ui.install_size_text.setStyleSheet("font-style: normal; font-weight: bold") self.ui.verify_button.setEnabled(self.options_changed) self.ui.cancel_button.setEnabled(True) if pf.system() == "Windows" or ArgumentsSingleton().debug: if dl_item.download.igame.prereq_info and not dl_item.download.igame.prereq_info.get("installed", False): self.advanced.ui.install_prereqs_check.setEnabled(True) self.advanced.ui.install_prereqs_label.setEnabled(True) self.advanced.ui.install_prereqs_check.setChecked(True) prereq_name = dl_item.download.igame.prereq_info.get("name", "") prereq_path = os.path.split(dl_item.download.igame.prereq_info.get("path", ""))[-1] prereq_desc = prereq_name if prereq_name else prereq_path self.advanced.ui.install_prereqs_check.setText( self.tr("Also install: {}").format(prereq_desc) ) if self.options.silent: self.close() def on_worker_failed(self, message: str): error_text = self.tr("Error") self.ui.download_size_text.setText(error_text) self.ui.install_size_text.setText(error_text) self.error_box(error_text, message) self.ui.verify_button.setEnabled(self.options_changed) self.ui.cancel_button.setEnabled(True) if self.options.silent: self.show() def error_box(self, label: str = "", message: str = ""): self.ui.warning_label.setVisible(bool(label)) self.ui.warning_label.setText(label) self.ui.warning_text.setVisible(bool(message)) self.ui.warning_text.setText(message) def on_worker_finished(self): self.worker_running = False # lk: happens when close() is called, also when top right 'X' is pressed. # lk: reject any events not coming from the buttons in case the WM # lk: doesn't honor the window hints def closeEvent(self, a0: QCloseEvent) -> None: if self.reject_close: a0.ignore() else: self.threadpool.clear() self.threadpool.waitForDone() self.result_ready.emit(InstallQueueItemModel(options=self.options, download=self.__download)) super(InstallDialog, self).closeEvent(a0) def keyPressEvent(self, e: QKeyEvent) -> None: if e.key() == Qt.Key_Escape: self.cancel_clicked() class InstallInfoWorker(QRunnable): class Signals(QObject): result = pyqtSignal(InstallQueueItemModel) failed = pyqtSignal(str) finished = pyqtSignal() def __init__(self, core: LegendaryCore, options: InstallOptionsModel): sys.excepthook = sys.__excepthook__ super(InstallInfoWorker, self).__init__() self.setAutoDelete(True) self.signals = InstallInfoWorker.Signals() self.core = core self.options = options @pyqtSlot() def run(self): try: if not self.options.overlay: cli = LegendaryCLI(self.core) status = LgndrIndirectStatus() result = cli.install_game( LgndrInstallGameArgs(**self.options.as_install_kwargs(), indirect_status=status) ) if result: download = InstallDownloadModel(*result) else: raise LgndrException(status.message) else: if not os.path.exists(path := self.options.base_path): os.makedirs(path) dlm, analysis, igame = self.core.prepare_overlay_install( path=self.options.base_path ) download = InstallDownloadModel( dlm=dlm, analysis=analysis, igame=igame, game=EOSOverlayApp, repair=False, repair_file="", res=ConditionCheckResult(), # empty ) if not download.res or not download.res.failures: self.signals.result.emit(InstallQueueItemModel(options=self.options, download=download)) else: self.signals.failed.emit("\n".join(str(i) for i in download.res.failures)) except LgndrException as ret: self.signals.failed.emit(ret.message) except Exception as e: self.signals.failed.emit(str(e)) self.signals.finished.emit() class TagCheckBox(QCheckBox): def __init__(self, text, tags: List[str], parent=None): super(TagCheckBox, self).__init__(parent) self.setText(text) self.tags = tags def isChecked(self) -> Union[bool, List[str]]: return self.tags if super(TagCheckBox, self).isChecked() else False