diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py
index 4b005515..76aaac46 100644
--- a/rare/components/tabs/games/game_info/game_info.py
+++ b/rare/components/tabs/games/game_info/game_info.py
@@ -1,13 +1,33 @@
import os
import platform
+import shutil
+from pathlib import Path
from logging import getLogger
+from typing import Tuple
-from PyQt5.QtCore import pyqtSignal, QThreadPool
-from PyQt5.QtWidgets import QWidget, QMessageBox
+from PyQt5.QtCore import Qt, pyqtSignal, QThreadPool, pyqtSlot
+from PyQt5.QtWidgets import (
+ QCheckBox,
+ QFileDialog,
+ QHBoxLayout,
+ QLabel,
+ QMenu,
+ QProgressBar,
+ QPushButton,
+ QVBoxLayout,
+ QWidget,
+ QMessageBox,
+ QWidgetAction,
+)
from legendary.models.game import Game, InstalledGame
-from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
+from rare.shared import (
+ LegendaryCoreSingleton,
+ GlobalSignalsSingleton,
+ ArgumentsSingleton,
+)
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.utils.models import InstallOptionsModel
from rare.utils.steam_grades import SteamWorker
@@ -53,7 +73,24 @@ class GameInfo(QWidget, Ui_GameInfo):
else:
self.repair_button.clicked.connect(self.repair)
- self.install_button.clicked.connect(lambda: self.game_utils.launch_game(self.game.app_name))
+ self.install_button.clicked.connect(
+ lambda: self.game_utils.launch_game(self.game.app_name)
+ )
+
+ self.move_game_pop_up = MoveGamePopUp()
+ self.move_action = QWidgetAction(self)
+ self.move_action.setDefaultWidget(self.move_game_pop_up)
+ self.move_button.setMenu(QMenu())
+ self.move_button.menu().addAction(self.move_action)
+ self.widget_container = QWidget()
+ box_layout = QHBoxLayout()
+ box_layout.setContentsMargins(0, 0, 0, 0)
+ box_layout.addWidget(self.move_button)
+ self.widget_container.setLayout(box_layout)
+ index = self.move_stack.addWidget(self.widget_container)
+ self.move_stack.setCurrentIndex(index)
+ self.move_game_pop_up.move_clicked.connect(self.move_game)
+ self.move_game_pop_up.browse_done.connect(self.show_menu_after_browse)
def uninstall(self):
if self.game_utils.uninstall_game(self.game.app_name):
@@ -135,6 +172,53 @@ class GameInfo(QWidget, Ui_GameInfo):
self.verify_widget.setCurrentIndex(0)
self.verify_threads.pop(app_name)
+ @pyqtSlot(str)
+ def move_game(self, destination_path):
+ destination_path = Path(destination_path)
+ install_path = Path(self.igame.install_path)
+ destination_path_with_suffix = destination_path.joinpath(
+ install_path.stem
+ )
+
+ progress_of_moving = QProgressBar(self)
+ progress_of_moving.setValue(0)
+
+ for i in destination_path.iterdir():
+ if install_path.stem in i.stem:
+ warn_msg = QMessageBox()
+ warn_msg.setText(self.tr("Destination file/directory exists."))
+ warn_msg.setInformativeText(
+ self.tr(
+ "Do you really want to overwrite it? This will delete {}"
+ ).format(destination_path_with_suffix)
+ )
+ warn_msg.addButton(QPushButton(self.tr("Yes")), QMessageBox.YesRole)
+ warn_msg.addButton(QPushButton(self.tr("No")), QMessageBox.NoRole)
+
+ response = warn_msg.exec()
+
+ if response == 0:
+ # Not using pathlib, since we can't delete not-empty folders. With shutil we can.
+ if destination_path_with_suffix.is_dir():
+ shutil.rmtree(destination_path_with_suffix)
+ else:
+ destination_path_with_suffix.unlink()
+ else:
+ return
+
+ shutil.move(self.igame.install_path, destination_path)
+
+ self.install_path.setText(str(destination_path_with_suffix))
+ self.igame.install_path = str(destination_path_with_suffix)
+ self.core.lgd.set_installed_game(self.igame.app_name, self.igame)
+ self.move_game_pop_up.install_path = self.igame.install_path
+
+ self.move_game_pop_up.refresh_indicator()
+ progress_of_moving.setValue(100)
+
+ def show_menu_after_browse(self):
+ self.move_button.showMenu()
+
def update_game(self, app_name: str):
self.game = self.core.get_game(app_name)
self.igame = self.core.get_installed_game(self.game.app_name)
@@ -193,13 +277,139 @@ class GameInfo(QWidget, Ui_GameInfo):
QThreadPool.globalInstance().start(self.steam_worker)
if len(self.verify_threads.keys()) == 0 or not self.verify_threads.get(
- self.game.app_name
+ self.game.app_name
):
self.verify_widget.setCurrentIndex(0)
elif self.verify_threads.get(self.game.app_name):
self.verify_widget.setCurrentIndex(1)
self.verify_progress.setValue(
- int(self.verify_threads[self.game.app_name].num
+ int(
+ self.verify_threads[self.game.app_name].num
/ self.verify_threads[self.game.app_name].total
- * 100)
+ * 100
+ )
)
+ 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()
+
+ bottom_layout = QHBoxLayout()
+ bottom_layout.setAlignment(Qt.AlignRight)
+ bottom_layout.addWidget(self.warn_overwriting, stretch=1)
+ bottom_layout.addWidget(self.move_game)
+
+ layout.addWidget(self.move_path_edit)
+ 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.
+ def find_mount(self, 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.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.")
+
+ current_path = Path(self.install_path).resolve()
+ destination_path = Path(dir_selected).resolve()
+ destination_path_with_suffix = destination_path.joinpath(
+ current_path.stem
+ ).resolve()
+
+ if not destination_path.is_dir():
+ return helper_func("Directory doesn't exist or file selected.")
+
+ 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 (
+ current_path == destination_path
+ or current_path == destination_path_with_suffix
+ ):
+ return helper_func("Same directory or parent directory selected.")
+
+ if str(current_path) in str(destination_path):
+ return helper_func(
+ "You can't select a directory that is inside the current install path."
+ )
+
+ if str(destination_path_with_suffix) in str(current_path):
+ return helper_func(
+ "You can't select a directory which contains the game installation."
+ )
+
+ if not platform.system() == "Windows":
+ if self.find_mount(destination_path) != self.find_mount(current_path):
+ return helper_func(
+ "Moving to a different drive is currently not supported."
+ )
+ else:
+ if current_path.drive != destination_path.drive:
+ return helper_func(
+ "Moving to a different drive is currently not supported."
+ )
+
+ 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."
+ )
+
+ for i in destination_path.iterdir():
+ if current_path.stem in i.stem:
+ self.warn_overwriting.setHidden(False)
+
+ # 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))
diff --git a/rare/ui/components/tabs/games/game_info/game_info.py b/rare/ui/components/tabs/games/game_info/game_info.py
index e3c23b1d..b5a57ee4 100644
--- a/rare/ui/components/tabs/games/game_info/game_info.py
+++ b/rare/ui/components/tabs/games/game_info/game_info.py
@@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameInfo(object):
def setupUi(self, GameInfo):
GameInfo.setObjectName("GameInfo")
- GameInfo.resize(414, 340)
+ GameInfo.resize(791, 583)
self.layout_game_info = QtWidgets.QHBoxLayout(GameInfo)
self.layout_game_info.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.layout_game_info.setObjectName("layout_game_info")
@@ -136,6 +136,28 @@ class Ui_GameInfo(object):
self.repair_button = QtWidgets.QPushButton(self.installed_page)
self.repair_button.setObjectName("repair_button")
self.installed_layout.addWidget(self.repair_button)
+ self.move_stack = QtWidgets.QStackedWidget(self.installed_page)
+ self.move_stack.setMinimumSize(QtCore.QSize(0, 20))
+ self.move_stack.setObjectName("move_stack")
+ self.page = QtWidgets.QWidget()
+ self.page.setObjectName("page")
+ self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.page)
+ self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+ self.move_button = QtWidgets.QToolButton(self.page)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.move_button.sizePolicy().hasHeightForWidth())
+ self.move_button.setSizePolicy(sizePolicy)
+ self.move_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
+ self.move_button.setObjectName("move_button")
+ self.horizontalLayout_2.addWidget(self.move_button)
+ self.move_stack.addWidget(self.page)
+ self.page_2 = QtWidgets.QWidget()
+ self.page_2.setObjectName("page_2")
+ self.move_stack.addWidget(self.page_2)
+ self.installed_layout.addWidget(self.move_stack)
self.uninstall_button = QtWidgets.QPushButton(self.installed_page)
self.uninstall_button.setStyleSheet("")
self.uninstall_button.setObjectName("uninstall_button")
@@ -219,7 +241,8 @@ class Ui_GameInfo(object):
self.lbl_install_path.setText(_translate("GameInfo", "Installation Path"))
self.lbl_install_size.setText(_translate("GameInfo", "Installation Size"))
self.verify_button.setText(_translate("GameInfo", "Verify Installation"))
- self.repair_button.setText(_translate("GameInfo", "Repair Instalation"))
+ self.repair_button.setText(_translate("GameInfo", "Repair Installation"))
+ self.move_button.setText(_translate("GameInfo", "Move Installation"))
self.uninstall_button.setText(_translate("GameInfo", "Uninstall Game"))
self.install_button.setText(_translate("GameInfo", "Install Game"))
self.lbl_version.setText(_translate("GameInfo", "Version"))
diff --git a/rare/ui/components/tabs/games/game_info/game_info.ui b/rare/ui/components/tabs/games/game_info/game_info.ui
index 4c35b941..73b9be19 100644
--- a/rare/ui/components/tabs/games/game_info/game_info.ui
+++ b/rare/ui/components/tabs/games/game_info/game_info.ui
@@ -6,8 +6,8 @@
0
0
- 414
- 340
+ 791
+ 583
@@ -283,10 +283,53 @@
-
- Repair Instalation
+ Repair Installation
+ -
+
+
+
+ 0
+ 20
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Move Installation
+
+
+ QToolButton::InstantPopup
+
+
+
+
+
+
+
+
-