1
0
Fork 0
mirror of synced 2024-06-23 08:40:45 +12:00

Merge pull request #233 from Dummerle/install_advanced_options

Install advanced options
This commit is contained in:
Dummerle 2022-08-27 12:47:09 +02:00 committed by GitHub
commit 159732b214
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 687 additions and 570 deletions

View file

@ -10,17 +10,18 @@ from legendary.models.downloading import ConditionCheckResult
from legendary.models.game import Game
from legendary.utils.selective_dl import get_sdl_appname
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.api_arguments import LgndrInstallGameArgs
from rare.lgndr.api_exception import LgndrException
from rare.lgndr.api_monkeys import LgndrIndirectStatus
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.core import LegendaryCore
from rare.models.install import InstallDownloadModel, InstallQueueItemModel
from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
from rare.utils.extra_widgets import PathEdit
from rare.models.install import InstallDownloadModel, InstallQueueItemModel
from rare.utils.misc import get_size
from rare.utils import config_helper
from rare.utils.extra_widgets import PathEdit
from rare.utils.misc import get_size
from rare.widgets.collabsible_widget import CollabsibleWidget
class InstallDialog(QDialog, Ui_InstallDialog):
@ -41,6 +42,11 @@ class InstallDialog(QDialog, Ui_InstallDialog):
if not self.dl_item.options.overlay
else Game(app_name=self.app_name, app_title="Epic Overlay")
)
self.advanced_layout.setParent(None)
self.advanced_widget = CollabsibleWidget(
self.advanced_layout, self.tr("Advanced options"), parent=self
)
self.advanced_placeholder_layout.addWidget(self.advanced_widget)
self.game_path = self.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value", "")
@ -147,6 +153,8 @@ class InstallDialog(QDialog, Ui_InstallDialog):
self.verify_button.clicked.connect(self.verify_clicked)
self.install_button.clicked.connect(self.install_clicked)
self.install_preqs_check.setChecked(self.dl_item.options.install_preqs)
self.install_dialog_layout.setSizeConstraint(self.install_dialog_layout.SetFixedSize)
def execute(self):
@ -199,6 +207,8 @@ class InstallDialog(QDialog, Ui_InstallDialog):
self.dl_item.options.ignore_space = self.ignore_space_check.isChecked()
self.dl_item.options.no_install = self.download_only_check.isChecked()
self.dl_item.options.platform = self.platform_combo_box.currentText()
self.dl_item.options.install_preqs = self.install_preqs_check.isChecked()
self.dl_item.options.create_shortcut = self.shortcut_cb.isChecked()
if self.sdl_list_cbs:
self.dl_item.options.install_tag = [""]
for cb in self.sdl_list_cbs:
@ -280,6 +290,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
if dl_item.igame.prereq_info and not dl_item.igame.prereq_info.get("installed", False):
self.install_preqs_check.setVisible(True)
self.install_preqs_lbl.setVisible(True)
self.install_preqs_check.setChecked(True)
self.install_preqs_check.setText(
self.tr("Also install: {}").format(dl_item.igame.prereq_info.get("name", ""))
)

View file

@ -3,31 +3,25 @@ import platform
import shutil
from logging import getLogger
from pathlib import Path
from typing import Tuple
from PyQt5.QtCore import (
QObject,
QRunnable,
Qt,
pyqtSignal,
QThreadPool,
pyqtSlot,
)
from PyQt5.QtWidgets import (
QFileDialog,
QHBoxLayout,
QLabel,
QMenu,
QProgressBar,
QPushButton,
QVBoxLayout,
QWidget,
QMessageBox,
QWidgetAction,
)
from legendary.models.game import Game, InstalledGame, VerifyResult
from legendary.utils.lfs import validate_files
from legendary.models.game import Game, InstalledGame
from rare.models.install import InstallOptionsModel
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
@ -35,11 +29,9 @@ from rare.shared import (
)
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.extra_widgets import PathEdit
from rare.utils.legendary_utils import VerifyWorker
from rare.models.install import InstallOptionsModel
from rare.utils.steam_grades import SteamWorker
from rare.utils.misc import get_size
from rare.utils.steam_grades import SteamWorker
from rare.widgets.image_widget import ImageWidget
logger = getLogger("GameInfo")
@ -401,245 +393,3 @@ class GameInfo(QWidget, Ui_GameInfo):
self.move_game_pop_up.update_game(app_name)
class MoveGamePopUp(QWidget):
move_clicked = pyqtSignal(str)
browse_done = pyqtSignal()
def __init__(self):
super(MoveGamePopUp, self).__init__()
layout: QVBoxLayout = QVBoxLayout()
self.install_path = str()
self.core = LegendaryCoreSingleton()
self.move_path_edit = PathEdit(str(), QFileDialog.Directory, edit_func=self.edit_func_move_game)
self.move_path_edit.path_select.clicked.connect(self.emit_browse_done_signal)
self.move_game = QPushButton(self.tr("Move"))
self.move_game.setMaximumWidth(50)
self.move_game.clicked.connect(self.emit_move_game_signal)
self.warn_overwriting = QLabel()
middle_layout = QHBoxLayout()
middle_layout.setAlignment(Qt.AlignRight)
middle_layout.addWidget(self.warn_overwriting, stretch=1)
middle_layout.addWidget(self.move_game)
bottom_layout = QVBoxLayout()
self.aval_space_label = QLabel()
self.req_space_label = QLabel()
bottom_layout.addWidget(self.aval_space_label)
bottom_layout.addWidget(self.req_space_label)
layout.addWidget(self.move_path_edit)
layout.addLayout(middle_layout)
layout.addLayout(bottom_layout)
self.setLayout(layout)
def emit_move_game_signal(self):
self.move_clicked.emit(self.move_path_edit.text())
def emit_browse_done_signal(self):
self.browse_done.emit()
def refresh_indicator(self):
# needed so the edit_func gets run again
text = self.move_path_edit.text()
self.move_path_edit.setText(str())
self.move_path_edit.setText(text)
# Thanks to lk.
@staticmethod
def find_mount(path):
mount_point = path
while path != path.anchor:
if path.is_mount():
return path
else:
path = path.parent
return mount_point
def edit_func_move_game(self, dir_selected):
self.move_game.setEnabled(True)
self.warn_overwriting.setHidden(True)
def helper_func(reason: str) -> Tuple[bool, str, str]:
self.move_game.setEnabled(False)
return False, dir_selected, self.tr(reason)
if not self.install_path or not dir_selected:
return helper_func("You need to provide a directory.")
install_path = Path(self.install_path).resolve()
dest_path = Path(dir_selected).resolve()
dest_path_with_suffix = dest_path.joinpath(install_path.stem).resolve()
if not dest_path.is_dir():
return helper_func("Directory doesn't exist or file selected.")
# Get free space on drive and size of game folder
_, _, free_space = shutil.disk_usage(dest_path)
source_size = sum(f.stat().st_size for f in install_path.glob("**/*") if f.is_file())
# Calculate from bytes to gigabytes
free_space_dest_drive = round(free_space / 1000**3, 2)
source_size = round(source_size / 1000**3, 2)
self.aval_space_label.setText(self.tr("Available space: {}GB".format(free_space_dest_drive)))
self.req_space_label.setText(self.tr("Required space: {}GB").format(source_size))
if not os.access(dir_selected, os.W_OK) or not os.access(self.install_path, os.W_OK):
return helper_func("No write permission on destination path/current install path.")
if install_path == dest_path or install_path == dest_path_with_suffix:
return helper_func("Same directory or parent directory selected.")
if str(install_path) in str(dest_path):
return helper_func("You can't select a directory that is inside the current install path.")
if str(dest_path_with_suffix) in str(install_path):
return helper_func("You can't select a directory which contains the game installation.")
for game in self.core.get_installed_list():
if game.install_path in dir_selected:
return helper_func("Game installations cannot be nested due to unintended sideeffects.")
is_existing_dir = is_game_dir(install_path, dest_path_with_suffix)
for i in dest_path.iterdir():
if install_path.stem in i.stem:
if dest_path_with_suffix.is_dir():
if not is_existing_dir:
self.warn_overwriting.setHidden(False)
elif dest_path_with_suffix.is_file():
self.warn_overwriting.setHidden(False)
if free_space_dest_drive <= source_size and not is_existing_dir:
return helper_func("Not enough space available on drive.")
# Fallback
self.move_game.setEnabled(True)
return True, dir_selected, str()
def update_game(self, app_name):
igame = self.core.get_installed_game(app_name, False)
if igame is None:
return
self.install_path = igame.install_path
self.move_path_edit.setText(igame.install_path)
self.warn_overwriting.setText(
self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem)
)
class CopyGameInstallation(QRunnable):
class Signals(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(str)
no_space_left = pyqtSignal()
def __init__(
self,
install_path: str,
dest_path: Path,
is_existing_dir: bool,
igame: InstalledGame,
):
super(CopyGameInstallation, self).__init__()
self.signals = self.Signals()
self.install_path = install_path
self.dest_path = dest_path
self.source_size = 0
self.dest_size = 0
self.is_existing_dir = is_existing_dir
self.core = LegendaryCoreSingleton()
self.igame = igame
self.file_list = None
self.total: int = 0
def run(self):
root_directory = Path(self.install_path)
self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file())
# if game dir is not existing, just copying:
if not self.is_existing_dir:
shutil.copytree(
self.install_path,
self.dest_path,
copy_function=self.copy_each_file_with_progress,
dirs_exist_ok=True,
)
else:
manifest_data, _ = self.core.get_installed_manifest(self.igame.app_name)
manifest = self.core.load_manifest(manifest_data)
files = sorted(
manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower(),
)
self.file_list = [(f.filename, f.sha_hash.hex()) for f in files]
self.total = len(self.file_list)
# recreate dir structure
shutil.copytree(
self.install_path,
self.dest_path,
copy_function=self.copy_dir_structure,
dirs_exist_ok=True,
)
for i, (result, relative_path, _, _) in enumerate(
validate_files(str(self.dest_path), self.file_list)
):
dst_path = f"{self.dest_path}/{relative_path}"
src_path = f"{self.install_path}/{relative_path}"
if Path(src_path).is_file():
if result == VerifyResult.HASH_MISMATCH:
try:
shutil.copy(src_path, dst_path)
except IOError:
self.signals.no_space_left.emit()
return
elif result == VerifyResult.FILE_MISSING:
try:
shutil.copy(src_path, dst_path)
except (IOError, OSError):
self.signals.no_space_left.emit()
return
elif result == VerifyResult.OTHER_ERROR:
logger.warning(f"Copying file {src_path} to {dst_path} failed")
self.signals.progress.emit(int(i * 10 / self.total * 10))
else:
logger.warning(
f"Source dir does not have file {src_path}. File will be missing in the destination "
f"dir. "
)
shutil.rmtree(self.install_path)
self.signals.finished.emit(str(self.dest_path))
def copy_each_file_with_progress(self, src, dst):
shutil.copy(src, dst)
self.dest_size += Path(src).stat().st_size
self.signals.progress.emit(int(self.dest_size * 10 / self.source_size * 10))
# This method is a copy_func, and only copies the src if it's a dir.
# Thus, it can be used to re-create the dir strucute.
@staticmethod
def copy_dir_structure(src, dst):
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
if os.path.isdir(src):
shutil.copyfile(src, dst)
shutil.copystat(src, dst)
return dst
def is_game_dir(install_path: Path, dest_path: Path):
# This iterates over the destination dir, then iterates over the current install dir and if the file names
# matches, we have an exisiting dir
if dest_path.is_dir():
for file in dest_path.iterdir():
for install_file in install_path.iterdir():
if file.name == install_file.name:
return True
return False

View file

@ -30,54 +30,37 @@ class GameSettings(DefaultGameSettings):
"",
file_type=QFileDialog.DirectoryOnly,
placeholder=self.tr("Cloud save path"),
edit_func=lambda text: (os.path.exists(text), text, PathEdit.reasons.dir_not_exist),
edit_func=lambda text: (True, text, None)
if os.path.exists(text)
else (False, text, PathEdit.reasons.dir_not_exist),
save_func=self.save_save_path,
)
self.cloud_layout.addRow(
QLabel(self.tr("Save path")), self.cloud_save_path_edit
)
self.cloud_layout.addRow(QLabel(self.tr("Save path")), self.cloud_save_path_edit)
self.compute_save_path_button = QPushButton(
icon("fa.magic"), self.tr("Auto compute save path")
)
self.compute_save_path_button.setSizePolicy(
QSizePolicy.Maximum, QSizePolicy.Fixed
)
self.compute_save_path_button = QPushButton(icon("fa.magic"), self.tr("Auto compute save path"))
self.compute_save_path_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.compute_save_path_button.clicked.connect(self.compute_save_path)
self.cloud_layout.addRow(None, self.compute_save_path_button)
self.offline.currentIndexChanged.connect(
lambda x: self.update_combobox(x, "offline")
)
self.skip_update.currentIndexChanged.connect(
lambda x: self.update_combobox(x, "skip_update_check")
)
self.offline.currentIndexChanged.connect(lambda x: self.update_combobox(x, "offline"))
self.skip_update.currentIndexChanged.connect(lambda x: self.update_combobox(x, "skip_update_check"))
self.cloud_sync.stateChanged.connect(
lambda: self.settings.setValue(
f"{self.game.app_name}/auto_sync_cloud", self.cloud_sync.isChecked()
)
)
self.override_exe_edit.textChanged.connect(
lambda text: self.save_line_edit("override_exe", text)
)
self.launch_params.textChanged.connect(
lambda x: self.save_line_edit("start_params", x)
)
self.override_exe_edit.textChanged.connect(lambda text: self.save_line_edit("override_exe", text))
self.launch_params.textChanged.connect(lambda x: self.save_line_edit("start_params", x))
self.game_settings_layout.setAlignment(Qt.AlignTop)
def compute_save_path(self):
if (
self.core.is_installed(self.game.app_name)
and self.game.supports_cloud_saves
):
if self.core.is_installed(self.game.app_name) and self.game.supports_cloud_saves:
try:
new_path = self.core.get_save_path(self.game.app_name)
except Exception as e:
logger.warning(str(e))
resolver = WineResolver(
get_raw_save_path(self.game), self.game.app_name
)
resolver = WineResolver(get_raw_save_path(self.game), self.game.app_name)
if not resolver.wine_env.get("WINEPREFIX"):
self.cloud_save_path_edit.setText("")
QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
@ -87,18 +70,14 @@ class GameSettings(DefaultGameSettings):
self.compute_save_path_button.setDisabled(True)
app_name = self.game.app_name[:]
resolver.signals.result_ready.connect(
lambda x: self.wine_resolver_finished(x, app_name)
)
resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
QThreadPool.globalInstance().start(resolver)
return
else:
self.cloud_save_path_edit.setText(new_path)
def wine_resolver_finished(self, path, app_name):
logger.info(
f"Wine resolver finished for {app_name}. Computed save path: {path}"
)
logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
if app_name == self.game.app_name:
self.cloud_save_path_edit.setDisabled(False)
self.compute_save_path_button.setDisabled(False)
@ -110,9 +89,9 @@ class GameSettings(DefaultGameSettings):
QMessageBox.warning(
None,
"Error",
self.tr(
"Error while launching {}. No permission to create {}"
).format(self.game.app_title, path),
self.tr("Error while launching {}. No permission to create {}").format(
self.game.app_title, path
),
)
return
if not path:
@ -160,9 +139,7 @@ class GameSettings(DefaultGameSettings):
self.igame = self.core.get_installed_game(self.game.app_name)
if self.igame:
if self.igame.can_run_offline:
offline = self.core.lgd.config.get(
self.game.app_name, "offline", fallback="unset"
)
offline = self.core.lgd.config.get(self.game.app_name, "offline", fallback="unset")
if offline == "true":
self.offline.setCurrentIndex(1)
elif offline == "false":
@ -176,9 +153,7 @@ class GameSettings(DefaultGameSettings):
else:
self.offline.setEnabled(False)
skip_update = self.core.lgd.config.get(
self.game.app_name, "skip_update_check", fallback="unset"
)
skip_update = self.core.lgd.config.get(self.game.app_name, "skip_update_check", fallback="unset")
if skip_update == "true":
self.skip_update.setCurrentIndex(1)
elif skip_update == "false":
@ -198,18 +173,14 @@ class GameSettings(DefaultGameSettings):
self.cloud_save_path_edit.setText("")
else:
self.cloud_group.setEnabled(True)
sync_cloud = self.settings.value(
f"{self.game.app_name}/auto_sync_cloud", True, bool
)
sync_cloud = self.settings.value(f"{self.game.app_name}/auto_sync_cloud", True, bool)
self.cloud_sync.setChecked(sync_cloud)
if self.igame.save_path:
self.cloud_save_path_edit.setText(self.igame.save_path)
else:
self.cloud_save_path_edit.setText("")
self.launch_params.setText(
self.core.lgd.config.get(self.game.app_name, "start_params", fallback="")
)
self.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
self.override_exe_edit.setText(
self.core.lgd.config.get(self.game.app_name, "override_exe", fallback="")
)

View file

@ -0,0 +1,258 @@
import os
import shutil
from logging import getLogger
from pathlib import Path
from typing import Tuple
from PyQt5.QtCore import pyqtSignal, QRunnable, QObject, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog
from legendary.models.game import VerifyResult, InstalledGame
from legendary.utils.lfs import validate_files
from rare.shared import LegendaryCoreSingleton
from rare.utils.extra_widgets import PathEdit
logger = getLogger("MoveGame")
class MoveGamePopUp(QWidget):
move_clicked = pyqtSignal(str)
browse_done = pyqtSignal()
def __init__(self):
super(MoveGamePopUp, self).__init__()
layout: QVBoxLayout = QVBoxLayout()
self.install_path = str()
self.core = LegendaryCoreSingleton()
self.move_path_edit = PathEdit(str(), QFileDialog.Directory, edit_func=self.edit_func_move_game)
self.move_path_edit.path_select.clicked.connect(self.emit_browse_done_signal)
self.move_game = QPushButton(self.tr("Move"))
self.move_game.setMaximumWidth(50)
self.move_game.clicked.connect(self.emit_move_game_signal)
self.warn_overwriting = QLabel()
middle_layout = QHBoxLayout()
middle_layout.setAlignment(Qt.AlignRight)
middle_layout.addWidget(self.warn_overwriting, stretch=1)
middle_layout.addWidget(self.move_game)
bottom_layout = QVBoxLayout()
self.aval_space_label = QLabel()
self.req_space_label = QLabel()
bottom_layout.addWidget(self.aval_space_label)
bottom_layout.addWidget(self.req_space_label)
layout.addWidget(self.move_path_edit)
layout.addLayout(middle_layout)
layout.addLayout(bottom_layout)
self.setLayout(layout)
def emit_move_game_signal(self):
self.move_clicked.emit(self.move_path_edit.text())
def emit_browse_done_signal(self):
self.browse_done.emit()
def refresh_indicator(self):
# needed so the edit_func gets run again
text = self.move_path_edit.text()
self.move_path_edit.setText(str())
self.move_path_edit.setText(text)
# Thanks to lk.
@staticmethod
def find_mount(path):
mount_point = path
while path != path.anchor:
if path.is_mount():
return path
else:
path = path.parent
return mount_point
def edit_func_move_game(self, dir_selected):
self.move_game.setEnabled(True)
self.warn_overwriting.setHidden(True)
def helper_func(reason: str) -> Tuple[bool, str, str]:
self.move_game.setEnabled(False)
return False, dir_selected, self.tr(reason)
if not self.install_path or not dir_selected:
return helper_func("You need to provide a directory.")
install_path = Path(self.install_path).resolve()
dest_path = Path(dir_selected).resolve()
dest_path_with_suffix = dest_path.joinpath(install_path.stem).resolve()
if not dest_path.is_dir():
return helper_func("Directory doesn't exist or file selected.")
# Get free space on drive and size of game folder
_, _, free_space = shutil.disk_usage(dest_path)
source_size = sum(f.stat().st_size for f in install_path.glob("**/*") if f.is_file())
# Calculate from bytes to gigabytes
free_space_dest_drive = round(free_space / 1000 ** 3, 2)
source_size = round(source_size / 1000 ** 3, 2)
self.aval_space_label.setText(self.tr("Available space: {}GB".format(free_space_dest_drive)))
self.req_space_label.setText(self.tr("Required space: {}GB").format(source_size))
if not os.access(dir_selected, os.W_OK) or not os.access(self.install_path, os.W_OK):
return helper_func("No write permission on destination path/current install path.")
if install_path == dest_path or install_path == dest_path_with_suffix:
return helper_func("Same directory or parent directory selected.")
if str(install_path) in str(dest_path):
return helper_func("You can't select a directory that is inside the current install path.")
if str(dest_path_with_suffix) in str(install_path):
return helper_func("You can't select a directory which contains the game installation.")
for game in self.core.get_installed_list():
if game.install_path in dir_selected:
return helper_func("Game installations cannot be nested due to unintended sideeffects.")
is_existing_dir = is_game_dir(install_path, dest_path_with_suffix)
for i in dest_path.iterdir():
if install_path.stem in i.stem:
if dest_path_with_suffix.is_dir():
if not is_existing_dir:
self.warn_overwriting.setHidden(False)
elif dest_path_with_suffix.is_file():
self.warn_overwriting.setHidden(False)
if free_space_dest_drive <= source_size and not is_existing_dir:
return helper_func("Not enough space available on drive.")
# Fallback
self.move_game.setEnabled(True)
return True, dir_selected, str()
def update_game(self, app_name):
igame = self.core.get_installed_game(app_name, False)
if igame is None:
return
self.install_path = igame.install_path
self.move_path_edit.setText(igame.install_path)
self.warn_overwriting.setText(
self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem)
)
class CopyGameInstallation(QRunnable):
class Signals(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(str)
no_space_left = pyqtSignal()
def __init__(
self,
install_path: str,
dest_path: Path,
is_existing_dir: bool,
igame: InstalledGame,
):
super(CopyGameInstallation, self).__init__()
self.signals = CopyGameInstallation.Signals()
self.install_path = install_path
self.dest_path = dest_path
self.source_size = 0
self.dest_size = 0
self.is_existing_dir = is_existing_dir
self.core = LegendaryCoreSingleton()
self.igame = igame
self.file_list = None
self.total: int = 0
def run(self):
root_directory = Path(self.install_path)
self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file())
# if game dir is not existing, just copying:
if not self.is_existing_dir:
shutil.copytree(
self.install_path,
self.dest_path,
copy_function=self.copy_each_file_with_progress,
dirs_exist_ok=True,
)
else:
manifest_data, _ = self.core.get_installed_manifest(self.igame.app_name)
manifest = self.core.load_manifest(manifest_data)
files = sorted(
manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower(),
)
self.file_list = [(f.filename, f.sha_hash.hex()) for f in files]
self.total = len(self.file_list)
# recreate dir structure
shutil.copytree(
self.install_path,
self.dest_path,
copy_function=self.copy_dir_structure,
dirs_exist_ok=True,
)
for i, (result, relative_path, _, _) in enumerate(
validate_files(str(self.dest_path), self.file_list)
):
dst_path = f"{self.dest_path}/{relative_path}"
src_path = f"{self.install_path}/{relative_path}"
if Path(src_path).is_file():
if result == VerifyResult.HASH_MISMATCH:
try:
shutil.copy(src_path, dst_path)
except IOError:
self.signals.no_space_left.emit()
return
elif result == VerifyResult.FILE_MISSING:
try:
shutil.copy(src_path, dst_path)
except (IOError, OSError):
self.signals.no_space_left.emit()
return
elif result == VerifyResult.OTHER_ERROR:
logger.warning(f"Copying file {src_path} to {dst_path} failed")
self.signals.progress.emit(int(i * 10 / self.total * 10))
else:
logger.warning(
f"Source dir does not have file {src_path}. File will be missing in the destination "
f"dir. "
)
shutil.rmtree(self.install_path)
self.signals.finished.emit(str(self.dest_path))
def copy_each_file_with_progress(self, src, dst):
shutil.copy(src, dst)
self.dest_size += Path(src).stat().st_size
self.signals.progress.emit(int(self.dest_size * 10 / self.source_size * 10))
# This method is a copy_func, and only copies the src if it's a dir.
# Thus, it can be used to re-create the dir strucute.
@staticmethod
def copy_dir_structure(src, dst):
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
if os.path.isdir(src):
shutil.copyfile(src, dst)
shutil.copystat(src, dst)
return dst
def is_game_dir(install_path: Path, dest_path: Path):
# This iterates over the destination dir, then iterates over the current install dir and if the file names
# matches, we have an exisiting dir
if dest_path.is_dir():
for file in dest_path.iterdir():
for install_file in install_path.iterdir():
if file.name == install_file.name:
return True
return False

View file

@ -40,7 +40,7 @@ class PreLaunchThread(QRunnable):
def prepare_launch(self, app_name) -> Union[LaunchArgs, None]:
try:
args = get_launch_args(self.core, InitArgs(app_name))
except GameArgsError as e:
except Exception as e:
self.signals.error_occurred.emit(str(e))
return None
if not args:
@ -196,8 +196,11 @@ class GameProcessApp(RareApp):
def stop(self):
self.logger.info("Stopping server")
self.server.close()
self.server.deleteLater()
try:
self.server.close()
self.server.deleteLater()
except RuntimeError:
pass
self.exit_app.emit()

View file

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'rare/ui/components/dialogs/install_dialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
# Created by: PyQt5 UI code generator 5.15.7
#
# 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.
@ -14,11 +14,12 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_InstallDialog(object):
def setupUi(self, InstallDialog):
InstallDialog.setObjectName("InstallDialog")
InstallDialog.resize(406, 447)
InstallDialog.resize(530, 489)
InstallDialog.setWindowTitle("Rare")
self.install_dialog_layout = QtWidgets.QFormLayout(InstallDialog)
self.install_dialog_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.install_dialog_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.install_dialog_layout.setLabelAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter)
self.install_dialog_layout.setObjectName("install_dialog_layout")
self.install_dialog_label = QtWidgets.QLabel(InstallDialog)
self.install_dialog_label.setObjectName("install_dialog_label")
@ -29,38 +30,6 @@ class Ui_InstallDialog(object):
self.install_dir_layout = QtWidgets.QHBoxLayout()
self.install_dir_layout.setObjectName("install_dir_layout")
self.install_dialog_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.install_dir_layout)
self.max_workers_label = QtWidgets.QLabel(InstallDialog)
self.max_workers_label.setObjectName("max_workers_label")
self.install_dialog_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.max_workers_label)
self.max_workers_layout = QtWidgets.QHBoxLayout()
self.max_workers_layout.setObjectName("max_workers_layout")
self.max_workers_spin = QtWidgets.QSpinBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.max_workers_spin.sizePolicy().hasHeightForWidth())
self.max_workers_spin.setSizePolicy(sizePolicy)
self.max_workers_spin.setObjectName("max_workers_spin")
self.max_workers_layout.addWidget(self.max_workers_spin)
self.max_workers_info_label = QtWidgets.QLabel(InstallDialog)
font = QtGui.QFont()
font.setItalic(True)
self.max_workers_info_label.setFont(font)
self.max_workers_info_label.setObjectName("max_workers_info_label")
self.max_workers_layout.addWidget(self.max_workers_info_label)
self.install_dialog_layout.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.max_workers_layout)
self.force_download_label = QtWidgets.QLabel(InstallDialog)
self.force_download_label.setObjectName("force_download_label")
self.install_dialog_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.force_download_label)
self.force_download_check = QtWidgets.QCheckBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.force_download_check.sizePolicy().hasHeightForWidth())
self.force_download_check.setSizePolicy(sizePolicy)
self.force_download_check.setText("")
self.force_download_check.setObjectName("force_download_check")
self.install_dialog_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.force_download_check)
self.platform_label = QtWidgets.QLabel(InstallDialog)
self.platform_label.setObjectName("platform_label")
self.install_dialog_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.platform_label)
@ -72,17 +41,13 @@ class Ui_InstallDialog(object):
self.platform_combo_box.setSizePolicy(sizePolicy)
self.platform_combo_box.setObjectName("platform_combo_box")
self.install_dialog_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.platform_combo_box)
self.ignore_space_label = QtWidgets.QLabel(InstallDialog)
self.ignore_space_label.setObjectName("ignore_space_label")
self.install_dialog_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.ignore_space_label)
self.download_only_label = QtWidgets.QLabel(InstallDialog)
self.download_only_label.setObjectName("download_only_label")
self.install_dialog_layout.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.download_only_label)
self.shortcut_lbl = QtWidgets.QLabel(InstallDialog)
self.shortcut_lbl.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.shortcut_lbl.setObjectName("shortcut_lbl")
self.install_dialog_layout.setWidget(9, QtWidgets.QFormLayout.LabelRole, self.shortcut_lbl)
self.shortcut_cb = QtWidgets.QCheckBox(InstallDialog)
self.shortcut_cb.setText("")
self.shortcut_cb.setChecked(True)
self.shortcut_cb.setObjectName("shortcut_cb")
self.install_dialog_layout.setWidget(9, QtWidgets.QFormLayout.FieldRole, self.shortcut_cb)
self.sdl_list_label = QtWidgets.QLabel(InstallDialog)
@ -115,6 +80,9 @@ class Ui_InstallDialog(object):
self.install_size_info_label.setWordWrap(True)
self.install_size_info_label.setObjectName("install_size_info_label")
self.install_dialog_layout.setWidget(14, QtWidgets.QFormLayout.FieldRole, self.install_size_info_label)
self.warn_label = QtWidgets.QLabel(InstallDialog)
self.warn_label.setObjectName("warn_label")
self.install_dialog_layout.setWidget(15, QtWidgets.QFormLayout.LabelRole, self.warn_label)
self.warn_message = QtWidgets.QLabel(InstallDialog)
font = QtGui.QFont()
font.setItalic(True)
@ -124,9 +92,6 @@ class Ui_InstallDialog(object):
self.warn_message.setWordWrap(True)
self.warn_message.setObjectName("warn_message")
self.install_dialog_layout.setWidget(15, QtWidgets.QFormLayout.FieldRole, self.warn_message)
self.warn_label = QtWidgets.QLabel(InstallDialog)
self.warn_label.setObjectName("warn_label")
self.install_dialog_layout.setWidget(15, QtWidgets.QFormLayout.LabelRole, self.warn_label)
self.button_layout = QtWidgets.QHBoxLayout()
self.button_layout.setObjectName("button_layout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
@ -140,42 +105,35 @@ class Ui_InstallDialog(object):
self.install_button = QtWidgets.QPushButton(InstallDialog)
self.install_button.setObjectName("install_button")
self.button_layout.addWidget(self.install_button)
self.install_dialog_layout.setLayout(16, QtWidgets.QFormLayout.SpanningRole, self.button_layout)
self.install_preqs_lbl = QtWidgets.QLabel(InstallDialog)
self.install_preqs_lbl.setObjectName("install_preqs_lbl")
self.install_dialog_layout.setWidget(10, QtWidgets.QFormLayout.LabelRole, self.install_preqs_lbl)
self.install_preqs_check = QtWidgets.QCheckBox(InstallDialog)
font = QtGui.QFont()
font.setItalic(True)
self.install_preqs_check.setFont(font)
self.install_preqs_check.setText("")
self.install_preqs_check.setObjectName("install_preqs_check")
self.install_dialog_layout.setWidget(10, QtWidgets.QFormLayout.FieldRole, self.install_preqs_check)
self.ignore_space_check = QtWidgets.QCheckBox(InstallDialog)
self.install_dialog_layout.setLayout(17, QtWidgets.QFormLayout.SpanningRole, self.button_layout)
self.advanced_layout = QtWidgets.QFormLayout()
self.advanced_layout.setLabelAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter)
self.advanced_layout.setObjectName("advanced_layout")
self.max_workers_label = QtWidgets.QLabel(InstallDialog)
self.max_workers_label.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.max_workers_label.setObjectName("max_workers_label")
self.advanced_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.max_workers_label)
self.max_workers_layout = QtWidgets.QHBoxLayout()
self.max_workers_layout.setObjectName("max_workers_layout")
self.max_workers_spin = QtWidgets.QSpinBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ignore_space_check.sizePolicy().hasHeightForWidth())
self.ignore_space_check.setSizePolicy(sizePolicy)
sizePolicy.setHeightForWidth(self.max_workers_spin.sizePolicy().hasHeightForWidth())
self.max_workers_spin.setSizePolicy(sizePolicy)
self.max_workers_spin.setObjectName("max_workers_spin")
self.max_workers_layout.addWidget(self.max_workers_spin)
self.max_workers_info_label = QtWidgets.QLabel(InstallDialog)
font = QtGui.QFont()
font.setItalic(True)
self.ignore_space_check.setFont(font)
self.ignore_space_check.setObjectName("ignore_space_check")
self.install_dialog_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.ignore_space_check)
self.download_only_check = QtWidgets.QCheckBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.download_only_check.sizePolicy().hasHeightForWidth())
self.download_only_check.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setItalic(True)
self.download_only_check.setFont(font)
self.download_only_check.setObjectName("download_only_check")
self.install_dialog_layout.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.download_only_check)
self.max_workers_info_label.setFont(font)
self.max_workers_info_label.setObjectName("max_workers_info_label")
self.max_workers_layout.addWidget(self.max_workers_info_label)
self.advanced_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.max_workers_layout)
self.max_memory_label = QtWidgets.QLabel(InstallDialog)
self.max_memory_label.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.max_memory_label.setObjectName("max_memory_label")
self.install_dialog_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.max_memory_label)
self.advanced_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.max_memory_label)
self.max_memory_layout = QtWidgets.QHBoxLayout()
self.max_memory_layout.setObjectName("max_memory_layout")
self.max_memory_spin = QtWidgets.QSpinBox(InstallDialog)
@ -196,15 +154,70 @@ class Ui_InstallDialog(object):
self.max_memory_info_label.setFont(font)
self.max_memory_info_label.setObjectName("max_memory_info_label")
self.max_memory_layout.addWidget(self.max_memory_info_label)
self.install_dialog_layout.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.max_memory_layout)
self.advanced_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.max_memory_layout)
self.dl_optimizations_label = QtWidgets.QLabel(InstallDialog)
self.dl_optimizations_label.setObjectName("dl_optimizations_label")
self.install_dialog_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.dl_optimizations_label)
self.advanced_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.dl_optimizations_label)
self.dl_optimizations_check = QtWidgets.QCheckBox(InstallDialog)
self.dl_optimizations_check.setText("")
self.dl_optimizations_check.setChecked(False)
self.dl_optimizations_check.setObjectName("dl_optimizations_check")
self.install_dialog_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.dl_optimizations_check)
self.advanced_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.dl_optimizations_check)
self.force_download_label = QtWidgets.QLabel(InstallDialog)
self.force_download_label.setObjectName("force_download_label")
self.advanced_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.force_download_label)
self.force_download_check = QtWidgets.QCheckBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.force_download_check.sizePolicy().hasHeightForWidth())
self.force_download_check.setSizePolicy(sizePolicy)
self.force_download_check.setText("")
self.force_download_check.setObjectName("force_download_check")
self.advanced_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.force_download_check)
self.ignore_space_label = QtWidgets.QLabel(InstallDialog)
self.ignore_space_label.setObjectName("ignore_space_label")
self.advanced_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.ignore_space_label)
self.ignore_space_check = QtWidgets.QCheckBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ignore_space_check.sizePolicy().hasHeightForWidth())
self.ignore_space_check.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setItalic(True)
self.ignore_space_check.setFont(font)
self.ignore_space_check.setObjectName("ignore_space_check")
self.advanced_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.ignore_space_check)
self.download_only_label = QtWidgets.QLabel(InstallDialog)
self.download_only_label.setObjectName("download_only_label")
self.advanced_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.download_only_label)
self.download_only_check = QtWidgets.QCheckBox(InstallDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.download_only_check.sizePolicy().hasHeightForWidth())
self.download_only_check.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setItalic(True)
self.download_only_check.setFont(font)
self.download_only_check.setObjectName("download_only_check")
self.advanced_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.download_only_check)
self.install_preqs_lbl = QtWidgets.QLabel(InstallDialog)
self.install_preqs_lbl.setObjectName("install_preqs_lbl")
self.advanced_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.install_preqs_lbl)
self.install_preqs_check = QtWidgets.QCheckBox(InstallDialog)
font = QtGui.QFont()
font.setItalic(True)
self.install_preqs_check.setFont(font)
self.install_preqs_check.setText("")
self.install_preqs_check.setChecked(False)
self.install_preqs_check.setObjectName("install_preqs_check")
self.advanced_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.install_preqs_check)
self.install_dialog_layout.setLayout(16, QtWidgets.QFormLayout.SpanningRole, self.advanced_layout)
self.advanced_placeholder_layout = QtWidgets.QVBoxLayout()
self.advanced_placeholder_layout.setObjectName("advanced_placeholder_layout")
self.install_dialog_layout.setLayout(12, QtWidgets.QFormLayout.SpanningRole, self.advanced_placeholder_layout)
self.retranslateUi(InstallDialog)
QtCore.QMetaObject.connectSlotsByName(InstallDialog)
@ -213,30 +226,30 @@ class Ui_InstallDialog(object):
_translate = QtCore.QCoreApplication.translate
self.install_dialog_label.setText(_translate("InstallDialog", "error"))
self.install_dir_label.setText(_translate("InstallDialog", "Install directory"))
self.max_workers_label.setText(_translate("InstallDialog", "Max workers"))
self.max_workers_info_label.setText(_translate("InstallDialog", "Less is slower. (0: Default)"))
self.force_download_label.setText(_translate("InstallDialog", "Force redownload"))
self.platform_label.setText(_translate("InstallDialog", "Platform"))
self.ignore_space_label.setText(_translate("InstallDialog", "Ignore free space"))
self.download_only_label.setText(_translate("InstallDialog", "Download only"))
self.shortcut_lbl.setText(_translate("InstallDialog", "Create shortcut"))
self.sdl_list_label.setText(_translate("InstallDialog", "Optional packs"))
self.download_size_label.setText(_translate("InstallDialog", "Download size"))
self.download_size_info_label.setText(_translate("InstallDialog", "Click verify..."))
self.install_size_label.setText(_translate("InstallDialog", "Total install size"))
self.install_size_info_label.setText(_translate("InstallDialog", "Click verify..."))
self.warn_message.setText(_translate("InstallDialog", "None"))
self.warn_label.setText(_translate("InstallDialog", "Warning"))
self.warn_message.setText(_translate("InstallDialog", "None"))
self.cancel_button.setText(_translate("InstallDialog", "Cancel"))
self.verify_button.setText(_translate("InstallDialog", "Verify"))
self.install_button.setText(_translate("InstallDialog", "Install"))
self.install_preqs_lbl.setText(_translate("InstallDialog", "Install prerequisites"))
self.ignore_space_check.setText(_translate("InstallDialog", "Use with caution!"))
self.download_only_check.setText(_translate("InstallDialog", "Do not try to install."))
self.max_workers_label.setText(_translate("InstallDialog", "Max workers"))
self.max_workers_info_label.setText(_translate("InstallDialog", "Less is slower. (0: Default)"))
self.max_memory_label.setText(_translate("InstallDialog", "Max shared memory"))
self.max_memory_spin.setSuffix(_translate("InstallDialog", "MiB"))
self.max_memory_info_label.setText(_translate("InstallDialog", "Less is slower (0: Default)"))
self.dl_optimizations_label.setText(_translate("InstallDialog", "Enable reordering"))
self.force_download_label.setText(_translate("InstallDialog", "Force redownload"))
self.ignore_space_label.setText(_translate("InstallDialog", "Ignore free space"))
self.ignore_space_check.setText(_translate("InstallDialog", "Use with caution!"))
self.download_only_label.setText(_translate("InstallDialog", "Download only"))
self.download_only_check.setText(_translate("InstallDialog", "Do not try to install."))
self.install_preqs_lbl.setText(_translate("InstallDialog", "Install prerequisites"))
if __name__ == "__main__":

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>406</width>
<height>447</height>
<width>530</width>
<height>489</height>
</rect>
</property>
<property name="windowTitle">
@ -37,59 +37,6 @@
<item row="1" column="1">
<layout class="QHBoxLayout" name="install_dir_layout"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="max_workers_label">
<property name="text">
<string>Max workers</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="max_workers_layout">
<item>
<widget class="QSpinBox" name="max_workers_spin">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="max_workers_info_label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Less is slower. (0: Default)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="force_download_label">
<property name="text">
<string>Force redownload</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="force_download_check">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="platform_label">
<property name="text">
@ -107,25 +54,14 @@
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="ignore_space_label">
<property name="text">
<string>Ignore free space</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="download_only_label">
<property name="text">
<string>Download only</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="shortcut_lbl">
<property name="text">
<string>Create shortcut</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="9" column="1">
@ -133,6 +69,9 @@
<property name="text">
<string notr="true"/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="0">
@ -198,6 +137,13 @@
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="warn_label">
<property name="text">
<string>Warning</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QLabel" name="warn_message">
<property name="font">
@ -219,14 +165,7 @@
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="warn_label">
<property name="text">
<string>Warning</string>
</property>
</widget>
</item>
<item row="16" column="0" colspan="2">
<item row="17" column="0" colspan="2">
<layout class="QHBoxLayout" name="button_layout">
<item>
<spacer name="button_hspacer">
@ -264,125 +203,211 @@
</item>
</layout>
</item>
<item row="10" column="0">
<widget class="QLabel" name="install_preqs_lbl">
<property name="text">
<string>Install prerequisites</string>
<item row="16" column="0" colspan="2">
<layout class="QFormLayout" name="advanced_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="install_preqs_check">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="ignore_space_check">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Use with caution!</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="download_only_check">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Do not try to install.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="max_memory_label">
<property name="text">
<string>Max shared memory</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="max_memory_layout">
<item>
<widget class="QSpinBox" name="max_memory_spin">
<item row="0" column="0">
<widget class="QLabel" name="max_workers_label">
<property name="text">
<string>Max workers</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="max_workers_layout">
<item>
<widget class="QSpinBox" name="max_workers_spin">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="max_workers_info_label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Less is slower. (0: Default)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="max_memory_label">
<property name="text">
<string>Max shared memory</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="max_memory_layout">
<item>
<widget class="QSpinBox" name="max_memory_spin">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string>MiB</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10240</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="max_memory_info_label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Less is slower (0: Default)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="dl_optimizations_label">
<property name="text">
<string>Enable reordering</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="dl_optimizations_check">
<property name="text">
<string notr="true"/>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="force_download_label">
<property name="text">
<string>Force redownload</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="force_download_check">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string>MiB</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10240</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="max_memory_info_label">
<item row="5" column="0">
<widget class="QLabel" name="ignore_space_label">
<property name="text">
<string>Ignore free space</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="ignore_space_check">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Less is slower (0: Default)</string>
<string>Use with caution!</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="download_only_label">
<property name="text">
<string>Download only</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="download_only_check">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Do not try to install.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="install_preqs_lbl">
<property name="text">
<string>Install prerequisites</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="install_preqs_check">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="dl_optimizations_label">
<property name="text">
<string>Enable reordering</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="dl_optimizations_check">
<property name="text">
<string notr="true"/>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
<item row="12" column="0" colspan="2">
<layout class="QVBoxLayout" name="advanced_placeholder_layout"/>
</item>
</layout>
</widget>

View file

@ -0,0 +1,86 @@
from PyQt5.QtCore import QParallelAnimationGroup, Qt, QPropertyAnimation, QAbstractAnimation
from PyQt5.QtWidgets import QWidget, QFrame, QToolButton, QGridLayout, QSizePolicy, QLayout
# https://newbedev.com/how-to-make-an-expandable-collapsable-section-widget-in-qt
class CollabsibleWidget(QWidget):
def __init__(
self, child_layout: QLayout = None, title: str = "", animation_duration: int = 200, parent=None
):
"""
References:
# Adapted from c++ version
https://stackoverflow.com/questions/32476006/how-to-make-an-expandable-collapsable-section-widget-in-qt
"""
super(CollabsibleWidget, self).__init__(parent=parent)
self.animationDuration = animation_duration
self.toggleAnimation = QParallelAnimationGroup()
self.contentArea = QWidget()
self.headerLine = QFrame()
self.toggleButton = QToolButton()
self.mainLayout = QGridLayout()
toggleButton = self.toggleButton
toggleButton.setStyleSheet("QToolButton { border: none; }")
toggleButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
toggleButton.setArrowType(Qt.RightArrow)
toggleButton.setText(str(title))
toggleButton.setCheckable(True)
toggleButton.setChecked(False)
headerLine = self.headerLine
headerLine.setFrameShape(QFrame.HLine)
headerLine.setFrameShadow(QFrame.Sunken)
headerLine.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }")
self.contentArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# start out collapsed
self.contentArea.setMaximumHeight(0)
self.contentArea.setMinimumHeight(0)
# let the entire widget grow and shrink with its content
toggleAnimation = self.toggleAnimation
toggleAnimation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
toggleAnimation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
toggleAnimation.addAnimation(QPropertyAnimation(self.contentArea, b"maximumHeight"))
# don't waste space
mainLayout = self.mainLayout
mainLayout.setVerticalSpacing(0)
mainLayout.setContentsMargins(0, 0, 0, 0)
row = 0
mainLayout.addWidget(self.toggleButton, row, 0, 1, 1, Qt.AlignLeft)
mainLayout.addWidget(self.headerLine, row, 2, 1, 1)
row += 1
mainLayout.addWidget(self.contentArea, row, 0, 1, 3)
self.setLayout(self.mainLayout)
def start_animation(checked):
arrow_type = Qt.DownArrow if checked else Qt.RightArrow
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward
toggleButton.setArrowType(arrow_type)
self.toggleAnimation.setDirection(direction)
self.toggleAnimation.start()
self.toggleButton.clicked.connect(start_animation)
if child_layout:
self.setContentLayout(child_layout)
def setContentLayout(self, content_layout: QLayout):
# Not sure if this is equivalent to self.contentArea.destroy()
self.contentArea.destroy()
self.contentArea.setLayout(content_layout)
collapsedHeight = self.sizeHint().height() - self.contentArea.maximumHeight()
contentHeight = content_layout.sizeHint().height()
for i in range(self.toggleAnimation.animationCount() - 1):
spoilerAnimation = self.toggleAnimation.animationAt(i)
spoilerAnimation.setDuration(self.animationDuration)
spoilerAnimation.setStartValue(collapsedHeight)
spoilerAnimation.setEndValue(collapsedHeight + contentHeight)
contentAnimation = self.toggleAnimation.animationAt(self.toggleAnimation.animationCount() - 1)
contentAnimation.setDuration(self.animationDuration)
contentAnimation.setStartValue(0)
contentAnimation.setEndValue(contentHeight)