1
0
Fork 0
mirror of synced 2024-06-26 10:11:19 +12:00

GameWidget: Implement reactive and interactive labels

The `status_label` displays what is currently going on with the game.
It reflects the current operation running on it or if it requires special
attention (update, needs verification etc)

The `tooltip_label` displays hover information such as what happens
if a part of the widget is clicked or in the case of the launch button if
the game can run (without version check, offline etc)

The context menu on the widgets will be updated and populated according
to the installation state of the game. Since the context menu was revised
the shortcut creation code was revised too to make it more compact.

the `create_desktop_link` and `get_rare_executable` functions are moved
from `rare.utils.misc` to `rare.utils.paths` to avoid cyclical imports and
better grouping. Two functions are added, `desktop_link_path` to uniformly
calculate the path of the shortcut and `desktop_links_supported` which
checks if Rare supports creating shortcuts on the current platform.
`desktop_links_supported` should be used as safeguard before `desktop_link_path`.

Desktop links are currently untested on Windows but if `shortcut.Description`
works as expected, it should be good to go.
This commit is contained in:
loathingKernel 2023-02-07 13:41:59 +02:00
parent 2903c19667
commit 1721677e33
13 changed files with 447 additions and 434 deletions

View file

@ -65,13 +65,13 @@ def main():
args = parser.parse_args()
if args.desktop_shortcut or args.startmenu_shortcut:
from rare.utils.misc import create_desktop_link
from rare.utils.paths import create_desktop_link
if args.desktop_shortcut:
create_desktop_link(type_of_link="desktop", for_rare=True)
create_desktop_link(app_name="rare_shortcut", link_type="desktop")
if args.startmenu_shortcut:
create_desktop_link(type_of_link="start_menu", for_rare=True)
create_desktop_link(app_name="rare_shortcut", link_type="start_menu")
print("Link created")
return

View file

@ -1,4 +1,5 @@
import datetime
import platform
from ctypes import c_uint64
from logging import getLogger
from typing import Union, Optional
@ -18,7 +19,8 @@ from rare.models.install import InstallOptionsModel, InstallQueueItemModel, Unin
from rare.shared import RareCore
from rare.shared.workers.install_info import InstallInfoWorker
from rare.shared.workers.uninstall import UninstallWorker
from rare.utils.misc import get_size, create_desktop_link
from rare.utils.misc import get_size
from rare.utils.paths import create_desktop_link, desktop_links_supported
from .download import DownloadWidget
from .groups import UpdateGroup, QueueGroup
from .thread import DlThread, DlResultModel, DlResultCode
@ -220,15 +222,18 @@ class DownloadsTab(QWidget):
@pyqtSlot(DlResultModel)
def __on_download_result(self, result: DlResultModel):
if result.code == DlResultCode.FINISHED:
if result.shortcuts:
if not create_desktop_link(result.options.app_name, self.core, "desktop"):
logger.info(f"Download finished: {result.options.app_name}")
if result.shortcut and desktop_links_supported():
if not create_desktop_link(
app_name=result.options.app_name,
app_title=result.shortcut_title,
link_name=result.shortcut_name,
link_type="desktop",
):
# maybe add it to download summary, to show in finished downloads
pass
logger.error(f"Failed to create desktop link on {platform.system()}")
else:
logger.info("Desktop shortcut written")
logger.info(
f"Download finished: {result.options.app_name}"
)
logger.info(f"Created desktop link {result.shortcut_name} for {result.options.app_name}")
if result.options.overlay:
self.signals.application.overlay_installed.emit()
@ -239,8 +244,8 @@ class DownloadsTab(QWidget):
self.updates_group.set_widget_enabled(result.options.app_name, True)
elif result.code == DlResultCode.ERROR:
QMessageBox.warning(self, self.tr("Error"), self.tr("Download error: {}").format(result.message))
logger.error(f"Download error: {result.options.app_name} ({result.message})")
QMessageBox.warning(self, self.tr("Error"), self.tr("Download error: {}").format(result.message))
elif result.code == DlResultCode.STOPPED:
logger.info(f"Download stopped: {result.options.app_name}")

View file

@ -34,7 +34,9 @@ class DlResultModel:
dlcs: Optional[List[Dict]] = None
sync_saves: bool = False
tip_url: str = ""
shortcuts: bool = False
shortcut: bool = False
shortcut_name: str = ""
shortcut_title: str = ""
class DlThread(QThread):
@ -149,7 +151,9 @@ class DlThread(QThread):
)
if not self.item.options.update and self.item.options.create_shortcut:
result.shortcuts = True
result.shortcut = True
result.shortcut_name = self.rgame.folder_name
result.shortcut_title = self.rgame.app_title
self.__result_emit(result)

View file

@ -1,9 +1,8 @@
import os
import platform
from abc import abstractmethod
from abc import ABCMeta
from logging import getLogger
from PyQt5.QtCore import pyqtSignal, QStandardPaths, Qt, pyqtSlot
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QMessageBox, QAction, QLabel, QPushButton
@ -14,12 +13,19 @@ from rare.shared import (
ArgumentsSingleton,
ImageManagerSingleton,
)
from rare.utils.misc import create_desktop_link
from rare.utils.paths import desktop_links_supported, desktop_link_path, create_desktop_link
from .library_widget import LibraryWidget
logger = getLogger("GameWidget")
class GameWidgetUi(metaclass=ABCMeta):
status_label: QLabel
install_btn: QPushButton
launch_btn: QPushButton
tooltip_label: QLabel
class GameWidget(LibraryWidget):
show_info = pyqtSignal(RareGame)
@ -33,86 +39,41 @@ class GameWidget(LibraryWidget):
self.rgame: RareGame = rgame
self.setContextMenuPolicy(Qt.ActionsContextMenu)
if self.rgame.is_installed or self.rgame.is_origin:
self.launch_action = QAction(self.tr("Launch"), self)
self.launch_action.triggered.connect(self._launch)
self.addAction(self.launch_action)
else:
self.install_action = QAction(self.tr("Install"), self)
self.install_action.triggered.connect(self._install)
self.addAction(self.install_action)
# if self.rgame.game.supports_cloud_saves:
# sync = QAction(self.tr("Sync with cloud"), self)
# sync.triggered.connect(self.sync_game)
# self.addAction(sync)
self.launch_action = QAction(self.tr("Launch"), self)
self.launch_action.triggered.connect(self._launch)
desktop = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
if os.path.exists(
os.path.join(desktop, f"{self.rgame.app_title}.desktop")
) or os.path.exists(os.path.join(desktop, f"{self.rgame.app_title}.lnk")):
self.create_desktop = QAction(self.tr("Remove Desktop link"))
else:
self.create_desktop = QAction(self.tr("Create Desktop link"))
if self.rgame.is_installed:
self.create_desktop.triggered.connect(
lambda: self.create_desktop_link("desktop")
)
self.addAction(self.create_desktop)
self.install_action = QAction(self.tr("Install"), self)
self.install_action.triggered.connect(self._install)
applications = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation)
if platform.system() == "Linux":
start_menu_file = os.path.join(applications, f"{self.rgame.app_title}.desktop")
elif platform.system() == "Windows":
start_menu_file = os.path.join(applications, "..", f"{self.rgame.app_title}.lnk")
else:
start_menu_file = ""
if platform.system() in ["Windows", "Linux"]:
if os.path.exists(start_menu_file):
self.create_start_menu = QAction(self.tr("Remove start menu link"))
else:
self.create_start_menu = QAction(self.tr("Create start menu link"))
if self.rgame.is_installed:
self.create_start_menu.triggered.connect(
lambda: self.create_desktop_link("start_menu")
)
self.addAction(self.create_start_menu)
# self.sync_action = QAction(self.tr("Sync with cloud"), self)
# self.sync_action.triggered.connect(self.sync_saves)
reload_image = QAction(self.tr("Reload Image"), self)
reload_image.triggered.connect(self._on_reload_image)
self.addAction(reload_image)
self.desktop_link_action = QAction(self)
self.desktop_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "desktop")
)
if self.rgame.is_installed and not self.rgame.is_origin:
self.uninstall_action = QAction(self.tr("Uninstall"), self)
self.uninstall_action.triggered.connect(self._uninstall)
self.addAction(self.uninstall_action)
self.menu_link_action = QAction(self)
self.menu_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "start_menu")
)
self.texts = {
"hover": {
"update_available": self.tr("Start without version check"),
"launch": self.tr("Launch Game"),
"launch_origin": self.tr("Launch/Link"),
"running": self.tr("Game running"),
"launch_offline": self.tr("Launch offline"),
"no_launch": self.tr("Can't launch game")
},
"status": {
"needs_verification": self.tr("Please verify game before playing"),
"running": self.tr("Game running"),
"syncing": self.tr("Syncing cloud saves"),
"update_available": self.tr("Update available"),
"no_meta": self.tr("Game is only offline available"),
"no_launch": self.tr("Can't launch game"),
},
}
self.reload_action = QAction(self.tr("Reload Image"), self)
self.reload_action.triggered.connect(self._on_reload_image)
self.uninstall_action = QAction(self.tr("Uninstall"), self)
self.uninstall_action.triggered.connect(self._uninstall)
self.update_actions()
# signals
self.rgame.signals.widget.update.connect(
lambda: self.setPixmap(self.rgame.pixmap)
)
self.rgame.signals.widget.update.connect(
self.update_widget
)
self.rgame.signals.widget.update.connect(lambda: self.setPixmap(self.rgame.pixmap))
self.rgame.signals.widget.update.connect(self.update_buttons)
self.rgame.signals.widget.update.connect(self.update_state)
self.rgame.signals.game.installed.connect(self.update_actions)
self.rgame.signals.game.uninstalled.connect(self.update_actions)
self.rgame.signals.progress.start.connect(
lambda: self.showProgress(
self.image_manager.get_pixmap(self.rgame.app_name, True),
@ -125,67 +86,113 @@ class GameWidget(LibraryWidget):
self.rgame.signals.progress.finish.connect(
lambda e: self.hideProgress(e)
)
self.rgame.signals.progress.finish.connect(self.set_status)
@abstractmethod
def set_status(self, label: QLabel):
if self.rgame.is_installed:
self.state_strings = {
RareGame.State.IDLE: "",
RareGame.State.RUNNING: self.tr("Running..."),
RareGame.State.DOWNLOADING: self.tr("Downloading..."),
RareGame.State.VERIFYING: self.tr("Verifying..."),
RareGame.State.MOVING: self.tr("Moving..."),
RareGame.State.UNINSTALLING: self.tr("Uninstalling..."),
"has_update": self.tr("Update available"),
"needs_verification": self.tr("Needs verification"),
"not_can_launch": self.tr("Can't launch"),
}
self.hover_strings = {
"info": self.tr("Show infromation"),
"install": self.tr("Install game"),
"can_launch": self.tr("Launch game"),
"is_foreign": self.tr("Launch offline"),
"has_update": self.tr("Launch without version check"),
"is_origin": self.tr("Launch/Link"),
"not_can_launch": self.tr("Can't launch"),
}
# lk: abstract class for typing, the `self.ui` attribute should be used
# lk: by the Ui class in the children. It must contain at least the same
# lk: attributes as `GameWidgetUi` class
self.ui = GameWidgetUi()
@pyqtSlot()
def update_state(self):
if self.rgame.is_idle:
if self.rgame.has_update:
label.setText(self.texts["status"]["update_available"])
return
if self.rgame.needs_verification:
label.setText(self.texts["status"]["needs_verification"])
return
label.setText("")
label.setVisible(False)
@abstractmethod
def update_widget(self, install_btn: QPushButton, launch_btn: QPushButton):
install_btn.setVisible(not self.rgame.is_installed)
install_btn.setEnabled(not self.rgame.is_installed)
launch_btn.setVisible(self.rgame.is_installed)
launch_btn.setEnabled(self.rgame.can_launch)
@property
def enterEventText(self) -> str:
if self.rgame.is_installed:
if self.rgame.state == RareGame.State.RUNNING:
return self.texts["status"]["running"]
elif (not self.rgame.is_origin) and self.rgame.needs_verification:
return self.texts["status"]["needs_verification"]
elif self.rgame.is_foreign:
return self.texts["hover"]["launch_offline"]
elif self.rgame.has_update:
return self.texts["hover"]["update_available"]
self.ui.status_label.setText(self.state_strings["has_update"])
elif self.rgame.needs_verification:
self.ui.status_label.setText(self.state_strings["needs_verification"])
elif not self.rgame.can_launch and self.rgame.is_installed:
self.ui.status_label.setText(self.state_strings["not_can_launch"])
else:
return self.tr("Game Info")
# return self.texts["hover"]["launch" if self.igame else "launch_origin"]
self.ui.status_label.setText(self.state_strings[self.rgame.state])
else:
if not self.rgame.state == RareGame.State.DOWNLOADING:
return self.tr("Game Info")
else:
return self.tr("Installation running")
self.ui.status_label.setText(self.state_strings[self.rgame.state])
self.ui.status_label.setVisible(bool(self.ui.status_label.text()))
@property
def leaveEventText(self) -> str:
if self.rgame.is_installed:
if self.rgame.state == RareGame.State.RUNNING:
return self.texts["status"]["running"]
# elif self.syncing_cloud_saves:
# return self.texts["status"]["syncing"]
elif self.rgame.is_foreign:
return self.texts["status"]["no_meta"]
elif self.rgame.has_update:
return self.texts["status"]["update_available"]
elif (not self.rgame.is_origin) and self.rgame.needs_verification:
return self.texts["status"]["needs_verification"]
else:
return ""
@pyqtSlot()
def update_buttons(self):
self.ui.install_btn.setVisible(not self.rgame.is_installed)
self.ui.install_btn.setEnabled(not self.rgame.is_installed)
self.ui.launch_btn.setVisible(self.rgame.is_installed)
self.ui.launch_btn.setEnabled(self.rgame.can_launch)
@pyqtSlot()
def update_actions(self):
for action in self.actions():
self.removeAction(action)
if self.rgame.is_installed or self.rgame.is_origin:
self.addAction(self.launch_action)
else:
if self.rgame.state == RareGame.State.DOWNLOADING:
return "Installation..."
self.addAction(self.install_action)
# if self.rgame.game.supports_cloud_saves:
# self.addAction(self.sync_action)
if desktop_links_supported() and self.rgame.is_installed:
if desktop_link_path(self.rgame.folder_name, "desktop").exists():
self.desktop_link_action.setText(self.tr("Remove Desktop link"))
else:
return ""
self.desktop_link_action.setText(self.tr("Create Desktop link"))
self.addAction(self.desktop_link_action)
if desktop_link_path(self.rgame.folder_name, "start_menu").exists():
self.menu_link_action.setText(self.tr("Remove Menu link"))
else:
self.menu_link_action.setText(self.tr("Create Menu link"))
self.addAction(self.menu_link_action)
self.addAction(self.reload_action)
if self.rgame.is_installed and not self.rgame.is_origin:
self.addAction(self.uninstall_action)
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
if a0 is self.ui.launch_btn:
if a1.type() == QEvent.Enter:
if not self.rgame.can_launch:
self.ui.tooltip_label.setText(self.hover_strings["not_can_launch"])
elif self.rgame.is_origin:
self.ui.tooltip_label.setText(self.hover_strings["is_origin"])
elif self.rgame.has_update:
self.ui.tooltip_label.setText(self.hover_strings["has_update"])
elif self.rgame.is_foreign and self.rgame.can_run_offline:
self.ui.tooltip_label.setText(self.hover_strings["is_foreign"])
elif self.rgame.can_launch:
self.ui.tooltip_label.setText(self.hover_strings["can_launch"])
return True
if a1.type() == QEvent.Leave:
self.ui.tooltip_label.setText(self.hover_strings["info"])
# return True
if a0 is self.ui.install_btn:
if a1.type() == QEvent.Enter:
self.ui.tooltip_label.setText(self.hover_strings["install"])
return True
if a1.type() == QEvent.Leave:
self.ui.tooltip_label.setText(self.hover_strings["info"])
# return True
if a0 is self:
if a1.type() == QEvent.Enter:
self.ui.tooltip_label.setText(self.hover_strings["info"])
return super().eventFilter(a0, a1)
def mousePressEvent(self, e: QMouseEvent) -> None:
# left button
@ -220,46 +227,42 @@ class GameWidget(LibraryWidget):
def _uninstall(self):
self.show_info.emit(self.rgame)
def create_desktop_link(self, type_of_link):
if type_of_link == "desktop":
shortcut_path = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
elif type_of_link == "start_menu":
shortcut_path = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation)
else:
return
if platform.system() == "Windows":
shortcut_path = os.path.join(shortcut_path, f"{self.rgame.app_title}.lnk")
elif platform.system() == "Linux":
shortcut_path = os.path.join(shortcut_path, f"{self.rgame.app_title}.desktop")
else:
def _create_link(self, name, link_type):
if not desktop_links_supported():
QMessageBox.warning(
self,
"Warning",
f"Create a Desktop link is currently not supported on {platform.system()}",
self.tr("Warning"),
self.tr("Creating shortcuts is currently unsupported on {}").format(platform.system()),
)
return
if not os.path.exists(shortcut_path):
try:
if not create_desktop_link(self.rgame.app_name, self.core, type_of_link):
return
except PermissionError:
QMessageBox.warning(
self, "Error", "Permission error. Cannot create Desktop Link"
)
if type_of_link == "desktop":
self.create_desktop.setText(self.tr("Remove Desktop link"))
elif type_of_link == "start_menu":
self.create_start_menu.setText(self.tr("Remove Start menu link"))
else:
if os.path.exists(shortcut_path):
os.remove(shortcut_path)
shortcut_path = desktop_link_path(name, link_type)
if type_of_link == "desktop":
self.create_desktop.setText(self.tr("Create Desktop link"))
elif type_of_link == "start_menu":
self.create_start_menu.setText(self.tr("Create Start menu link"))
if not shortcut_path.exists():
try:
if not create_desktop_link(
app_name=self.rgame.app_name,
app_title=self.rgame.app_title,
link_name=self.rgame.folder_name,
link_type=link_type,
):
raise PermissionError
except PermissionError:
QMessageBox.warning(self, "Error", "Could not create shortcut.")
return
if link_type == "desktop":
self.desktop_link_action.setText(self.tr("Remove Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Remove Start Menu link"))
else:
if shortcut_path.exists():
shortcut_path.unlink(missing_ok=True)
if link_type == "desktop":
self.desktop_link_action.setText(self.tr("Create Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Create Start Menu link"))
# def sync_finished(self, app_name):
# self.syncing_cloud_saves = False

View file

@ -1,7 +1,7 @@
from logging import getLogger
from typing import Optional
from PyQt5.QtCore import QEvent, pyqtSlot
from PyQt5.QtCore import QEvent
from rare.models.game import RareGame
from rare.shared.image_manager import ImageSize
@ -13,13 +13,13 @@ logger = getLogger("IconGameWidget")
class IconGameWidget(GameWidget):
def __init__(self, rgame: RareGame, parent=None):
super(IconGameWidget, self).__init__(rgame, parent)
super().__init__(rgame, parent)
self.setObjectName(f"{rgame.app_name}")
self.setFixedSize(ImageSize.Display)
self.ui = IconWidget()
self.ui.setupUi(self)
self.ui.title_label.setText(f"<h4>{self.rgame.app_title}</h4>")
self.ui.title_label.setText(self.rgame.app_title)
self.ui.launch_btn.clicked.connect(self._launch)
self.ui.launch_btn.setVisible(self.rgame.is_installed)
self.ui.install_btn.clicked.connect(self._install)
@ -27,27 +27,22 @@ class IconGameWidget(GameWidget):
self.ui.launch_btn.setEnabled(self.rgame.can_launch)
self.set_status()
self.update_state()
@pyqtSlot()
def set_status(self):
super(IconGameWidget, self).set_status(self.ui.status_label)
@pyqtSlot()
def update_widget(self):
super(IconGameWidget, self).update_widget(self.ui.install_btn, self.ui.launch_btn)
# lk: "connect" the buttons' enter/leave events to this widget
self.installEventFilter(self)
self.ui.launch_btn.installEventFilter(self)
self.ui.install_btn.installEventFilter(self)
def enterEvent(self, a0: Optional[QEvent] = None) -> None:
if a0 is not None:
a0.accept()
self.ui.tooltip_label.setText(self.enterEventText)
self.ui.enterAnimation(self)
def leaveEvent(self, a0: Optional[QEvent] = None) -> None:
if a0 is not None:
a0.accept()
self.ui.leaveAnimation(self)
self.ui.tooltip_label.setText(self.leaveEventText)
# def sync_finished(self, app_name):
# if not app_name == self.rgame.app_name:

View file

@ -17,15 +17,15 @@ from rare.widgets.elide_label import ElideLabel
class IconWidget(object):
def __init__(self):
self._effect = None
self._animation = None
self._animation: QPropertyAnimation = None
self.status_label = None
self.mini_widget = None
self.mini_effect = None
self.title_label = None
self.tooltip_label = None
self.launch_btn = None
self.install_btn = None
self.status_label: ElideLabel = None
self.mini_widget: QWidget = None
self.mini_effect: QGraphicsOpacityEffect = None
self.title_label: QLabel = None
self.tooltip_label: ElideLabel = None
self.launch_btn: QPushButton = None
self.install_btn: QPushButton = None
def setupUi(self, widget: QWidget):
# information at top
@ -34,6 +34,7 @@ class IconWidget(object):
self.status_label.setStyleSheet(
f"QLabel#{self.status_label.objectName()}"
"{"
"font-weight: bold;"
"color: white;"
"background-color: rgba(0, 0, 0, 65%);"
"border-radius: 5%;"
@ -68,11 +69,12 @@ class IconWidget(object):
self.title_label.setStyleSheet(
f"QLabel#{self.title_label.objectName()}"
"{"
"font-weight: bold;"
"background-color: rgba(0, 0, 0, 0%); color: white;"
"}"
)
self.title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.title_label.setAlignment(Qt.AlignTop)
self.title_label.setAlignment(Qt.AlignVCenter)
self.title_label.setAutoFillBackground(False)
self.title_label.setWordWrap(True)
@ -125,12 +127,6 @@ class IconWidget(object):
self.install_btn.setFixedSize(QSize(widget.width() // 4, widget.width() // 4))
# Create layouts
# layout for the whole widget, holds the image
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSizeConstraint(QVBoxLayout.SetFixedSize)
# layout for the image, holds the mini widget and a spacer item
image_layout = QVBoxLayout()
image_layout.setContentsMargins(2, 2, 2, 2)

View file

@ -37,6 +37,7 @@ class ProgressLabel(QLabel):
def setStyleSheetColors(self, bg: QColor, fg: QColor, brd: QColor):
sheet = (
f"QLabel#{type(self).__name__} {{"
f"background-color: rgba({bg.red()}, {bg.green()}, {bg.blue()}, 65%);"
f"color: rgb({fg.red()}, {fg.green()}, {fg.blue()});"
f"border-width: 1px;"
@ -44,6 +45,7 @@ class ProgressLabel(QLabel):
f"border-color: rgb({brd.red()}, {brd.green()}, {brd.blue()});"
f"font-weight: bold;"
f"font-size: 16pt;"
f"}}"
)
self.setStyleSheet(sheet)

View file

@ -1,13 +1,15 @@
from logging import getLogger
from PyQt5.QtCore import Qt, QEvent, QRect, pyqtSlot, pyqtSignal
from PyQt5.QtCore import Qt, QEvent, QRect
from PyQt5.QtGui import (
QPalette,
QBrush,
QPaintEvent,
QPainter,
QLinearGradient,
QPixmap, QImage, QResizeEvent,
QPixmap,
QImage,
QResizeEvent,
)
from rare.models.game import RareGame
@ -20,72 +22,45 @@ logger = getLogger("ListGameWidget")
class ListGameWidget(GameWidget):
def __init__(self, rgame: RareGame, parent=None):
super(ListGameWidget, self).__init__(rgame, parent)
super().__init__(rgame, parent)
self.setObjectName(f"{rgame.app_name}")
self.ui = ListWidget()
self.ui.setupUi(self)
self.ui.title_label.setText(f"<h3>{self.rgame.app_title}</h3>")
self.ui.install_btn.setVisible(not self.rgame.is_installed)
self.ui.title_label.setText(self.rgame.app_title)
self.ui.launch_btn.clicked.connect(self._launch)
self.ui.launch_btn.setVisible(self.rgame.is_installed)
self.ui.install_btn.clicked.connect(self._install)
self.ui.install_btn.setVisible(not self.rgame.is_installed)
self.ui.launch_btn.setEnabled(self.rgame.can_launch)
self.ui.launch_btn.setText(
self.tr("Launch") if not self.rgame.is_origin else self.tr("Link/Play")
)
self.ui.launch_btn.clicked.connect(self._launch)
self.ui.launch_btn.setVisible(self.rgame.is_installed)
self.ui.developer_text.setText(self.rgame.developer)
# self.version_label.setVisible(self.is_installed)
if self.rgame.igame:
self.ui.version_text.setText(self.rgame.version)
self.ui.size_text.setText(get_size(self.rgame.install_size) if self.rgame.install_size else "")
self.ui.launch_btn.setEnabled(self.rgame.can_launch)
self.update_state()
self.set_status()
@pyqtSlot()
def set_status(self):
super(ListGameWidget, self).set_status(self.ui.status_label)
@pyqtSlot()
def update_widget(self):
super(ListGameWidget, self).update_widget(self.ui.install_btn, self.ui.launch_btn)
def update_text(self, e=None):
if self.rgame.is_installed:
if self.rgame.has_update:
self.ui.status_label.setText(self.texts["status"]["update_available"])
elif self.rgame.is_foreign:
self.ui.status_label.setText(self.texts["status"]["no_meta"])
elif self.rgame.igame and self.rgame.needs_verification:
self.ui.status_label.setText(self.texts["status"]["needs_verification"])
# elif self.syncing_cloud_saves:
# self.ui.status_label.setText(self.texts["status"]["syncing"])
else:
self.ui.status_label.setText("")
self.ui.status_label.setVisible(False)
else:
self.ui.status_label.setVisible(False)
# lk: "connect" the buttons' enter/leave events to this widget
self.installEventFilter(self)
self.ui.launch_btn.installEventFilter(self)
self.ui.install_btn.installEventFilter(self)
def enterEvent(self, a0: QEvent = None) -> None:
if a0 is not None:
a0.accept()
status = self.enterEventText
self.ui.tooltip_label.setText(status)
self.ui.tooltip_label.setVisible(bool(status))
self.ui.tooltip_label.setVisible(True)
def leaveEvent(self, a0: QEvent = None) -> None:
if a0 is not None:
a0.accept()
status = self.leaveEventText
self.ui.tooltip_label.setText(status)
self.ui.tooltip_label.setVisible(bool(status))
self.ui.tooltip_label.setVisible(False)
# def game_started(self, app_name):
# if app_name == self.rgame.app_name:

View file

@ -29,10 +29,14 @@ class ListWidget(object):
def setupUi(self, widget: QWidget):
self.title_label = QLabel(parent=widget)
self.title_label.setWordWrap(False)
self.title_label.setStyleSheet(
"font-weight: bold;"
)
self.status_label = QLabel(parent=widget)
self.status_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.status_label.setStyleSheet(
"font-weight: bold;"
"background-color: rgba(0,0,0,75%);"
"border: 1px solid black;"
"border-radius: 5px;"

View file

@ -4,13 +4,12 @@ import subprocess
import sys
from logging import getLogger
from PyQt5.QtCore import QSettings, QStandardPaths, Qt
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import QWidget, QMessageBox
from rare.shared import LegendaryCoreSingleton
from rare.components.tabs.settings.widgets.rpc import RPCSettings
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
from rare.utils.paths import log_dir
from rare.utils.misc import (
get_translations,
get_color_schemes,
@ -18,8 +17,8 @@ from rare.utils.misc import (
get_style_sheets,
set_style_sheet,
get_size,
create_desktop_link,
)
from rare.utils.paths import create_desktop_link, desktop_link_path, log_dir, desktop_links_supported
logger = getLogger("RareSettings")
@ -106,36 +105,28 @@ class RareSettings(QWidget, Ui_RareSettings):
)
)
self.notification.stateChanged.connect(
lambda: self.settings.setValue(
"notification", self.notification.isChecked()
)
lambda: self.settings.setValue("notification", self.notification.isChecked())
)
self.save_size.stateChanged.connect(self.save_window_size)
self.log_games.stateChanged.connect(
lambda: self.settings.setValue("show_console", self.log_games.isChecked())
)
desktop = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
applications = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation)
if platform.system() == "Linux":
self.desktop_file = os.path.join(desktop, "Rare.desktop")
self.start_menu_link = os.path.join(applications, "Rare.desktop")
elif platform.system() == "Windows":
self.desktop_file = os.path.join(desktop, "Rare.lnk")
self.start_menu_link = os.path.join(applications, "..", "Rare.lnk")
if desktop_links_supported():
self.desktop_link = desktop_link_path("Rare", "desktop")
self.start_menu_link = desktop_link_path("Rare", "start_menu")
else:
self.desktop_link_btn.setText(self.tr("Not supported"))
self.desktop_link_btn.setToolTip(self.tr("Not supported"))
self.desktop_link_btn.setDisabled(True)
self.startmenu_link_btn.setText(self.tr("Not supported"))
self.startmenu_link_btn.setToolTip(self.tr("Not supported"))
self.startmenu_link_btn.setDisabled(True)
self.desktop_file = ""
self.desktop_link = ""
self.start_menu_link = ""
if self.desktop_file and os.path.exists(self.desktop_file):
if self.desktop_link and self.desktop_link.exists():
self.desktop_link_btn.setText(self.tr("Remove desktop link"))
if self.start_menu_link and os.path.exists(self.start_menu_link):
if self.start_menu_link and self.start_menu_link.exists():
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
self.desktop_link_btn.clicked.connect(self.create_desktop_link)
@ -171,7 +162,8 @@ class RareSettings(QWidget, Ui_RareSettings):
def create_start_menu_link(self):
try:
if not os.path.exists(self.start_menu_link):
create_desktop_link(type_of_link="start_menu", for_rare=True)
if not create_desktop_link(app_name="rare_shortcut", link_type="start_menu"):
return
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
else:
os.remove(self.start_menu_link)
@ -186,11 +178,12 @@ class RareSettings(QWidget, Ui_RareSettings):
def create_desktop_link(self):
try:
if not os.path.exists(self.desktop_file):
create_desktop_link(type_of_link="desktop", for_rare=True)
if not os.path.exists(self.desktop_link):
if not create_desktop_link(app_name="rare_shortcut", link_type="desktop"):
return
self.desktop_link_btn.setText(self.tr("Remove Desktop link"))
else:
os.remove(self.desktop_file)
os.remove(self.desktop_link)
self.desktop_link_btn.setText(self.tr("Create desktop link"))
except PermissionError as e:
logger.error(str(e))

View file

@ -14,8 +14,7 @@ from rare.lgndr.core import LegendaryCore
from rare.models.install import InstallOptionsModel, UninstallOptionsModel
from rare.shared.game_process import GameProcess
from rare.shared.image_manager import ImageManager
from rare.utils.misc import get_rare_executable
from rare.utils.paths import data_dir
from rare.utils.paths import data_dir, get_rare_executable
logger = getLogger("RareGame")

View file

@ -1,7 +1,5 @@
import os
import platform
import shlex
import sys
from logging import getLogger
from typing import List, Union, Type
@ -12,23 +10,19 @@ from PyQt5.QtCore import (
QObject,
QRunnable,
QSettings,
QStandardPaths,
QFile,
QDir, Qt,
QDir,
Qt,
)
from PyQt5.QtGui import QPalette, QColor, QImage, QFontMetrics
from PyQt5.QtWidgets import qApp, QStyleFactory, QWidget, QLabel
from PyQt5.QtGui import QPalette, QColor, QFontMetrics
from PyQt5.QtWidgets import qApp, QStyleFactory, QLabel
from PyQt5.sip import wrappertype
from legendary.core import LegendaryCore
from legendary.models.game import Game
from requests.exceptions import HTTPError
from rare.models.apiresults import ApiResults
from rare.utils.paths import image_dir, resources_path
if platform.system() == "Windows":
# noinspection PyUnresolvedReferences
from win32com.client import Dispatch # pylint: disable=E0401
from rare.utils.paths import resources_path
logger = getLogger("Utils")
settings = QSettings("Rare", "Rare")
@ -156,152 +150,6 @@ def get_size(b: Union[int, float]) -> str:
b /= 1024
def get_rare_executable() -> List[str]:
# lk: detect if nuitka
if "__compiled__" in globals():
executable = [sys.executable]
elif platform.system() == "Linux" or platform.system() == "Darwin":
if p := os.environ.get("APPIMAGE"):
executable = [p]
else:
if sys.executable == os.path.abspath(sys.argv[0]):
executable = [sys.executable]
else:
executable = [sys.executable, os.path.abspath(sys.argv[0])]
elif platform.system() == "Windows":
executable = [sys.executable]
if sys.executable != os.path.abspath(sys.argv[0]):
executable.append(os.path.abspath(sys.argv[0]))
if executable[0].endswith("python.exe"):
# be sure to start consoleless then
executable[0] = executable[0].replace("python.exe", "pythonw.exe")
if executable[1].endswith("rare"):
executable[1] = executable[1] + ".exe"
else:
executable = [sys.executable]
executable[0] = os.path.abspath(executable[0])
return executable
def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link="desktop",
for_rare: bool = False) -> bool:
if not for_rare:
igame = core.get_installed_game(app_name)
icon = os.path.join(os.path.join(image_dir(), igame.app_name, "installed.png"))
icon = icon.replace(".png", "")
if platform.system() == "Linux":
if type_of_link == "desktop":
path = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
elif type_of_link == "start_menu":
path = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation)
else:
return False
if not os.path.exists(path):
return False
executable = get_rare_executable()
executable = shlex.join(executable)
if for_rare:
with open(os.path.join(path, "Rare.desktop"), "w") as desktop_file:
desktop_file.write(
"[Desktop Entry]\n"
f"Name=Rare\n"
f"Type=Application\n"
f"Categories=Game;\n"
f"Icon={os.path.join(resources_path, 'images', 'Rare.png')}\n"
f"Exec={executable}\n"
"Terminal=false\n"
"StartupWMClass=Rare\n"
)
else:
with open(os.path.join(path, f"{igame.title}.desktop"), "w") as desktop_file:
desktop_file.write(
"[Desktop Entry]\n"
f"Name={igame.title}\n"
f"Type=Application\n"
f"Categories=Game;\n"
f"Icon={icon}.png\n"
f"Exec={executable} launch {app_name}\n"
"Terminal=false\n"
"StartupWMClass=Rare\n"
)
os.chmod(os.path.join(path, f"{igame.title}.desktop"), 0o755)
return True
elif platform.system() == "Windows":
# Target of shortcut
if type_of_link == "desktop":
target_folder = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
elif type_of_link == "start_menu":
target_folder = os.path.join(
QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation),
".."
)
else:
logger.warning("No valid type of link")
return False
if not os.path.exists(target_folder):
return False
if for_rare:
linkName = "Rare.lnk"
else:
linkName = igame.title
# TODO: this conversion is not applied everywhere (see base_installed_widget), should it?
for c in r'<>?":|\/*':
linkName = linkName.replace(c, "")
linkName = f"{linkName.strip()}.lnk"
# Path to location of link file
pathLink = os.path.join(target_folder, linkName)
# Add shortcut
shell = Dispatch("WScript.Shell")
shortcut = shell.CreateShortCut(pathLink)
executable = get_rare_executable()
arguments = []
if len(executable) > 1:
arguments.extend(executable[1:])
executable = executable[0]
if not for_rare:
arguments.extend(["launch", app_name])
shortcut.Targetpath = executable
# Maybe there is a better solution, but windows does not accept single quotes (Windows is weird)
shortcut.Arguments = shlex.join(arguments).replace("'", '"')
if for_rare:
shortcut.WorkingDirectory = QStandardPaths.writableLocation(QStandardPaths.HomeLocation)
# Icon
if for_rare:
icon_location = os.path.join(resources_path, "images", "Rare.ico")
else:
if not os.path.exists(f"{icon}.ico"):
img = QImage()
img.load(f"{icon}.png")
img.save(f"{icon}.ico")
logger.info("Created ico file")
icon_location = f"{icon}.ico"
shortcut.IconLocation = os.path.abspath(icon_location)
shortcut.save()
return True
# mac OS is based on Darwin
elif platform.system() == "Darwin":
return False
class CloudWorker(QRunnable):
class Signals(QObject):
# List[SaveGameFile]

View file

@ -1,8 +1,20 @@
import os
import platform
import shlex
import shutil
import sys
from logging import getLogger
from pathlib import Path
from typing import List
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtGui import QImage
if platform.system() == "Windows":
# noinspection PyUnresolvedReferences
from win32com.client import Dispatch # pylint: disable=E0401
logger = getLogger("Paths")
resources_path = Path(__file__).absolute().parent.parent.joinpath("resources")
@ -15,7 +27,7 @@ for old_dir in [
]:
if old_dir.exists():
# lk: case-sensitive matching on Winblows
if old_dir.stem in os.listdir(old_dir.parent):
if old_dir.stem in old_dir.parent.iterdir():
shutil.rmtree(old_dir, ignore_errors=True)
@ -49,3 +61,180 @@ def create_dirs() -> None:
for path in (data_dir(), cache_dir(), image_dir(), log_dir(), tmp_dir()):
if not path.exists():
path.mkdir(parents=True)
def home_dir() -> Path:
return Path(QStandardPaths.writableLocation(QStandardPaths.HomeLocation))
def desktop_dir() -> Path:
return Path(QStandardPaths.writableLocation(QStandardPaths.DesktopLocation))
def applications_dir() -> Path:
return Path(QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation))
__link_suffix = {
"Windows": ".lnk",
"Linux": ".desktop",
}
def desktop_links_supported() -> bool:
return platform.system() in __link_suffix.keys()
__link_type = {
"desktop": desktop_dir(),
# lk: for some undocumented reason, on Windows we used the parent directory
# lk: for start menu items. Mirror it here for backwards compatibility
"start_menu": applications_dir().parent if platform.system() == "Windows" else applications_dir(),
}
def desktop_link_path(link_name: str, link_type: str) -> Path:
"""
Creates the path of a shortcut link
:param link_name:
Name of the shortcut file
:param link_type:
"desktop" or "start_menu"
:return Path:
shortcut path
"""
return __link_type[link_type].joinpath(f"{link_name}{__link_suffix[platform.system()]}")
def get_rare_executable() -> List[str]:
# lk: detect if nuitka
if "__compiled__" in globals():
executable = [sys.executable]
elif platform.system() == "Linux" or platform.system() == "Darwin":
if p := os.environ.get("APPIMAGE"):
executable = [p]
else:
if sys.executable == os.path.abspath(sys.argv[0]):
executable = [sys.executable]
else:
executable = [sys.executable, os.path.abspath(sys.argv[0])]
elif platform.system() == "Windows":
executable = [sys.executable]
if sys.executable != os.path.abspath(sys.argv[0]):
executable.append(os.path.abspath(sys.argv[0]))
if executable[0].endswith("python.exe"):
# be sure to start consoleless then
executable[0] = executable[0].replace("python.exe", "pythonw.exe")
if executable[1].endswith("rare"):
executable[1] = executable[1] + ".exe"
else:
executable = [sys.executable]
executable[0] = os.path.abspath(executable[0])
return executable
def create_desktop_link(app_name: str, app_title: str = "", link_name: str = "", link_type="desktop") -> bool:
"""
Creates a desktop or start menu shortcut link
:param app_name:
app_name or "rare_shortcut" for Rare itself
:param app_title:
the title shown in the shortcut
(overrides to "Rare" for "rare_shortcut")
:param link_name:
the sanitized filename of the shortcut (use the folder_name attribute of RareGame)
(overrides to "Rare" for "rare_shortcut")
:param link_type:
"desktop" or "start_menu"
:return bool:
True if successful else False
"""
# macOS is based on Darwin
if not desktop_links_supported():
logger.error(f"Shortcut creation is not available on {platform.system()}")
return False
if link_type not in ["desktop", "start_menu"]:
logger.error(f"Invalid link type {link_type}")
return False
for_rare = app_name == "rare_shortcut"
if for_rare:
icon_path = resources_path.joinpath("images", "Rare.png")
if platform.system() == "Windows":
icon_path = resources_path.joinpath("images", "Rare.ico")
app_title = "Rare"
link_name = "Rare"
else:
icon_path = image_dir().joinpath(app_name, "installed.png")
if platform.system() == "Windows":
icon_path = image_dir().joinpath(app_name, "installed.ico")
if not icon_path.exists():
img = QImage()
img.load(str(icon_path.with_suffix(".png")))
img.save(str(icon_path))
logger.info("Created ico file")
if not app_title or not link_name:
logger.error("Missing app_title or link_name")
return False
shortcut_path = desktop_link_path(link_name, link_type)
if not shortcut_path.parent.exists():
logger.error(f"Parent directory {shortcut_path.parent} does not exist")
return False
else:
logger.info(f"Creating shortcut for {app_title} at {shortcut_path}")
if platform.system() == "Linux":
executable = get_rare_executable()
executable = shlex.join(executable)
if not for_rare:
executable = f"{executable} launch {app_name}"
with shortcut_path.open(mode="w") as desktop_file:
desktop_file.write(
"[Desktop Entry]\n"
f"Name={app_title}\n"
"Type=Application\n"
"Categories=Game;\n"
f"Icon={icon_path}\n"
f"Exec={executable}\n"
"Terminal=false\n"
"StartupWMClass=Rare\n"
)
# shortcut_path.chmod(0o755)
return True
elif platform.system() == "Windows":
# Add shortcut
shell = Dispatch("WScript.Shell")
shortcut = shell.CreateShortCut(str(shortcut_path))
executable = get_rare_executable()
arguments = []
if len(executable) > 1:
arguments.extend(executable[1:])
executable = executable[0]
if not for_rare:
arguments.extend(["launch", app_name])
shortcut.Targetpath = executable
# Maybe there is a better solution, but windows does not accept single quotes (Windows is weird)
shortcut.Arguments = shlex.join(arguments).replace("'", '"')
shortcut.Description = app_title
if for_rare:
shortcut.WorkingDirectory = str(home_dir())
# Icon
shortcut.IconLocation = icon_path.absolute()
shortcut.save()
return True