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

App: Move legendary initialization to the singleton

App: Move tray to MainWindow
Shared: Add destructor for singleton instances
This commit is contained in:
loathingKernel 2022-09-04 01:14:43 +03:00
parent 4951743bbf
commit afcdc1dea1
5 changed files with 194 additions and 162 deletions

View file

@ -13,14 +13,19 @@ from typing import Optional
import legendary
import requests.exceptions
from PyQt5.QtCore import QThreadPool, QTimer, QT_VERSION_STR, PYQT_VERSION_STR
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox
from PyQt5.QtWidgets import QApplication, QMessageBox
from requests import HTTPError
import rare
from rare.components.dialogs.launch_dialog import LaunchDialog
from rare.components.main_window import MainWindow
from rare.components.tray_icon import TrayIcon
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
ApiResultsSingleton,
clear_singleton_instance
)
from rare.shared.image_manager import ImageManagerSingleton
from rare.utils import legendary_utils, config_helper
from rare.utils.paths import cache_dir, tmp_dir
@ -54,64 +59,21 @@ def excepthook(exc_type, exc_value, exc_tb):
class App(RareApp):
mainwindow: Optional[MainWindow] = None
tray_icon: Optional[QSystemTrayIcon] = None
def __init__(self, args: Namespace):
super(App, self).__init__()
self.core = LegendaryCoreSingleton()
self.args = ArgumentsSingleton(args) # add some options
self.window_launched = False
# init Legendary
try:
self.core = LegendaryCoreSingleton(init=True)
except configparser.MissingSectionHeaderError as e:
logger.warning(f"Config is corrupt: {e}")
if config_path := os.environ.get("XDG_CONFIG_HOME"):
path = os.path.join(config_path, "legendary")
else:
path = os.path.expanduser("~/.config/legendary")
with open(os.path.join(path, "config.ini"), "w") as config_file:
config_file.write("[Legendary]")
self.core = LegendaryCoreSingleton(init=True)
if "Legendary" not in self.core.lgd.config.sections():
self.core.lgd.config.add_section("Legendary")
self.core.lgd.save_config()
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)
config_helper.init_config_handler(self.core)
# workaround if egl sync enabled, but no programdata_path
# programdata_path might be unset if logging in through the browser
if self.core.egl_sync_enabled:
if self.core.egl.programdata_path is None:
self.core.lgd.config.remove_option("Legendary", "egl_sync")
self.core.lgd.save_config()
else:
if not os.path.exists(self.core.egl.programdata_path):
self.core.lgd.config.remove_option("Legendary", "egl_sync")
self.core.lgd.save_config()
# set Application name for settings
self.launch_dialog = None
self.mainwindow: Optional[MainWindow] = None
self.launch_dialog: Optional[LaunchDialog] = None
self.signals = GlobalSignalsSingleton(init=True)
self.image_manager = ImageManagerSingleton(init=True)
self.signals.exit_app.connect(self.exit_app)
self.signals.send_notification.connect(
lambda title: self.tray_icon.showMessage(
self.tr("Download finished"),
self.tr("Download finished. {} is playable now").format(title),
QSystemTrayIcon.Information,
4000,
)
if self.settings.value("notification", True, bool)
else None
)
# launch app
self.launch_dialog = LaunchDialog(parent=None)
self.launch_dialog.quit_app.connect(self.launch_dialog.close)
@ -140,12 +102,6 @@ class App(RareApp):
td = abs(dt_exp - dt_now)
self.timer.start(int(td.total_seconds() - 60) * 1000)
def show_mainwindow(self):
if self.window_launched:
self.mainwindow.show()
else:
self.mainwindow.show_window_centered()
def start_app(self):
for igame in self.core.get_installed_list():
if not os.path.exists(igame.install_path):
@ -167,85 +123,27 @@ class App(RareApp):
logger.info(f"{igame.title} needs verification")
self.mainwindow = MainWindow()
self.tray_icon: TrayIcon = TrayIcon(self)
self.tray_icon.exit_action.triggered.connect(self.exit_app)
self.tray_icon.start_rare.triggered.connect(self.show_mainwindow)
self.tray_icon.activated.connect(
lambda r: self.show_mainwindow()
if r == QSystemTrayIcon.DoubleClick
else None
)
self.mainwindow.exit_app.connect(self.exit_app)
if not self.args.silent:
self.mainwindow.show_window_centered()
self.window_launched = True
if self.args.subparser == "launch":
if self.args.app_name in [
i.app_name for i in self.core.get_installed_list()
]:
logger.info(
f"Launching {self.core.get_installed_game(self.args.app_name).title}"
)
self.mainwindow.tab_widget.games_tab.game_utils.prepare_launch(
self.args.app_name
)
else:
logger.error(
f"Could not find {self.args.app_name} in Games or it is not installed"
)
QMessageBox.warning(
self.mainwindow,
"Warning",
self.tr(
"Could not find {} in installed games. Did you modify the shortcut? "
).format(self.args.app_name),
)
self.mainwindow.show()
if self.args.test_start:
self.exit_app(0)
def tray(self, reason):
if reason == QSystemTrayIcon.DoubleClick:
self.mainwindow.show()
logger.info("Show App")
def exit_app(self, exit_code=0):
# FIXME: Fix this with the download tab redesign
if self.mainwindow is not None:
if not self.args.offline and self.mainwindow.tab_widget.downloadTab.is_download_active:
question = QMessageBox.question(
self.mainwindow,
self.tr("Close"),
self.tr(
"There is a download active. Do you really want to exit app?"
),
QMessageBox.Yes,
QMessageBox.No,
)
if question == QMessageBox.No:
return
else:
# clear queue
self.mainwindow.tab_widget.downloadTab.queue_widget.update_queue([])
self.mainwindow.tab_widget.downloadTab.stop_download()
# FIXME: End of FIXME
self.mainwindow.timer.stop()
self.mainwindow.hide()
threadpool = QThreadPool.globalInstance()
threadpool.waitForDone()
self.core.exit()
if self.mainwindow is not None:
self.mainwindow.close()
self.mainwindow.deleteLater()
self.mainwindow = None
if self.tray_icon is not None:
self.tray_icon.deleteLater()
self.tray_icon = None
if self.timer is not None:
self.timer.stop()
self.timer.deleteLater()
self.timer = None
if self.mainwindow is not None:
self.mainwindow.close()
self.mainwindow = None
clear_singleton_instance(self.signals)
clear_singleton_instance(self.args)
clear_singleton_instance(ApiResultsSingleton())
self.processEvents()
shutil.rmtree(tmp_dir)
os.makedirs(tmp_dir)
@ -284,10 +182,16 @@ def start(args):
logger.info(f"Operating System: {platform.system()}")
while True:
core = LegendaryCoreSingleton(init=True)
config_helper.init_config_handler(core)
app = App(args)
exit_code = app.exec_()
# if not restart
# restart app
del app
core.exit()
clear_singleton_instance(core)
if exit_code != -133742:
break

View file

@ -1,18 +1,22 @@
import os
from logging import getLogger
from PyQt5.QtCore import Qt, QSettings, QTimer, QSize
from PyQt5.QtCore import Qt, QSettings, QTimer, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QCursor
from PyQt5.QtWidgets import QMainWindow, QApplication, QStatusBar, QScrollArea, QScroller, QComboBox
from PyQt5.QtWidgets import QMainWindow, QApplication, QStatusBar, QScrollArea, QScroller, QComboBox, QMessageBox
from rare.components.tabs import TabWidget
from rare.components.tray_icon import TrayIcon
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.utils.paths import data_dir
logger = getLogger("Window")
logger = getLogger("MainWindow")
class MainWindow(QMainWindow):
# int: exit code
exit_app: pyqtSignal = pyqtSignal(int)
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=parent)
self.setAttribute(Qt.WA_DeleteOnClose)
@ -46,8 +50,42 @@ class MainWindow(QMainWindow):
self.timer.timeout.connect(self.timer_finished)
self.timer.start(1000)
def show_window_centered(self):
self.show()
self.signals.exit_app.connect(self.on_exit_app)
self.exit_code = 0
self.accept_close = False
self.tray_icon: TrayIcon = TrayIcon(self)
self.tray_icon.exit_action.triggered.connect(self.on_exit_app)
self.tray_icon.start_rare.triggered.connect(self.show)
self.tray_icon.activated.connect(
lambda r: self.toggle()
if r == self.tray_icon.DoubleClick
else None
)
self.signals.send_notification.connect(
lambda title: self.tray_icon.showMessage(
self.tr("Download finished"),
self.tr("Download finished. {} is playable now").format(title),
self.tray_icon.Information,
4000,
)
if self.settings.value("notification", True, bool)
else None
)
self.window_launched = False
# enable kinetic scrolling
for scroll_area in self.findChildren(QScrollArea):
if not scroll_area.property("no_kinetic_scroll"):
QScroller.grabGesture(scroll_area.viewport(), QScroller.LeftMouseButtonGesture)
# fix scrolling
for combo_box in scroll_area.findChildren(QComboBox):
combo_box.wheelEvent = lambda e: e.ignore()
def center_window(self):
# get the margins of the decorated window
margins = self.windowHandle().frameMargins()
# get the screen the cursor is on
@ -68,14 +106,23 @@ class MainWindow(QMainWindow):
- self.rect().adjusted(0, 0, decor_width, decor_height).center()
)
# enable kinetic scrolling
for scroll_area in self.findChildren(QScrollArea):
if not scroll_area.property("no_kinetic_scroll"):
QScroller.grabGesture(scroll_area.viewport(), QScroller.LeftMouseButtonGesture)
def show(self) -> None:
super(MainWindow, self).show()
if not self.window_launched:
self.center_window()
self.window_launched = True
# fix scrolling
for combo_box in scroll_area.findChildren(QComboBox):
combo_box.wheelEvent = lambda e: e.ignore()
def hide(self) -> None:
if self.settings.value("save_size", False, bool):
size = self.size().width(), self.size().height()
self.settings.setValue("window_size", size)
super(MainWindow, self).hide()
def toggle(self):
if self.isHidden():
self.show()
else:
self.hide()
def timer_finished(self):
file_path = os.path.join(data_dir, "lockfile")
@ -88,15 +135,43 @@ class MainWindow(QMainWindow):
os.remove(file_path)
self.timer.start(1000)
@pyqtSlot()
@pyqtSlot(int)
def on_exit_app(self, exit_code=0) -> None:
# FIXME: Fix this with the download tab redesign
if not self.args.offline and self.tab_widget.downloadTab.is_download_active:
question = QMessageBox.question(
self,
self.tr("Close"),
self.tr(
"There is a download active. Do you really want to exit app?"
),
QMessageBox.Yes,
QMessageBox.No,
)
if question == QMessageBox.No:
return
else:
# clear queue
self.tab_widget.downloadTab.queue_widget.update_queue([])
self.tab_widget.downloadTab.stop_download()
# FIXME: End of FIXME
self.exit_code = exit_code
self.close()
def close(self) -> bool:
self.accept_close = True
return super(MainWindow, self).close()
def closeEvent(self, e: QCloseEvent):
if self.settings.value("save_size", False, bool):
size = self.size().width(), self.size().height()
self.settings.setValue("window_size", size)
if self.settings.value("sys_tray", True, bool):
self.hide()
e.ignore()
return
elif self.args.offline:
pass
self.signals.exit_app.emit(0)
e.ignore()
if not self.accept_close:
if self.settings.value("sys_tray", True, bool):
self.hide()
e.ignore()
return
self.timer.stop()
self.tray_icon.deleteLater()
self.hide()
self.exit_app.emit(self.exit_code)
super(MainWindow, self).closeEvent(e)
e.accept()

View file

@ -13,7 +13,7 @@ logger = getLogger("TrayIcon")
class TrayIcon(QSystemTrayIcon):
def __init__(self, parent):
super(TrayIcon, self).__init__(parent)
super(TrayIcon, self).__init__(parent=parent)
self.core = LegendaryCoreSingleton()
self.setIcon(QIcon(":/images/Rare.png"))
@ -33,7 +33,7 @@ class TrayIcon(QSystemTrayIcon):
if len(installed := self.core.get_installed_list()) < 5:
last_played = [GameMeta(i.app_name) for i in sorted(installed, key=lambda x: x.title)]
elif games := sorted(
parent.mainwindow.tab_widget.games_tab.game_utils.game_meta.get_games(),
parent.tab_widget.games_tab.game_utils.game_meta.get_games(),
key=lambda x: x.last_played, reverse=True):
last_played: List[GameMeta] = games[0:5]
else:
@ -46,7 +46,7 @@ class TrayIcon(QSystemTrayIcon):
a.setProperty("app_name", game.app_name)
self.game_actions.append(a)
a.triggered.connect(
lambda: parent.mainwindow.tab_widget.games_tab.game_utils.prepare_launch(
lambda: parent.tab_widget.games_tab.game_utils.prepare_launch(
self.sender().property("app_name"))
)

View file

@ -5,14 +5,18 @@ Each of the objects in this module should be instantiated ONCE
and only ONCE!
"""
import configparser
import logging
import os
from argparse import Namespace
from typing import Optional
from typing import Optional, Union
from rare.lgndr.core import LegendaryCore
from rare.models.apiresults import ApiResults
from rare.models.signals import GlobalSignals
logger = logging.getLogger("Singleton")
_legendary_core_singleton: Optional[LegendaryCore] = None
_global_signals_singleton: Optional[GlobalSignals] = None
_arguments_singleton: Optional[Namespace] = None
@ -23,8 +27,33 @@ def LegendaryCoreSingleton(init: bool = False) -> LegendaryCore:
global _legendary_core_singleton
if _legendary_core_singleton is None and not init:
raise RuntimeError("Uninitialized use of LegendaryCoreSingleton")
if _legendary_core_singleton is None:
_legendary_core_singleton = LegendaryCore()
if _legendary_core_singleton is not None and init:
raise RuntimeError("LegendaryCore already initialized")
if init:
try:
_legendary_core_singleton = LegendaryCore()
except configparser.MissingSectionHeaderError as e:
logger.warning(f"Config is corrupt: {e}")
if config_path := os.environ.get("XDG_CONFIG_HOME"):
path = os.path.join(config_path, "legendary")
else:
path = os.path.expanduser("~/.config/legendary")
with open(os.path.join(path, "config.ini"), "w") as config_file:
config_file.write("[Legendary]")
_legendary_core_singleton = LegendaryCore()
if "Legendary" not in _legendary_core_singleton.lgd.config.sections():
_legendary_core_singleton.lgd.config.add_section("Legendary")
_legendary_core_singleton.lgd.save_config()
# workaround if egl sync enabled, but no programdata_path
# programdata_path might be unset if logging in through the browser
if _legendary_core_singleton.egl_sync_enabled:
if _legendary_core_singleton.egl.programdata_path is None:
_legendary_core_singleton.lgd.config.remove_option("Legendary", "egl_sync")
_legendary_core_singleton.lgd.save_config()
else:
if not os.path.exists(_legendary_core_singleton.egl.programdata_path):
_legendary_core_singleton.lgd.config.remove_option("Legendary", "egl_sync")
_legendary_core_singleton.lgd.save_config()
return _legendary_core_singleton
@ -32,7 +61,9 @@ def GlobalSignalsSingleton(init: bool = False) -> GlobalSignals:
global _global_signals_singleton
if _global_signals_singleton is None and not init:
raise RuntimeError("Uninitialized use of GlobalSignalsSingleton")
if _global_signals_singleton is None:
if _global_signals_singleton is not None and init:
raise RuntimeError("GlobalSignals already initialized")
if init:
_global_signals_singleton = GlobalSignals()
return _global_signals_singleton
@ -41,7 +72,9 @@ def ArgumentsSingleton(args: Namespace = None) -> Optional[Namespace]:
global _arguments_singleton
if _arguments_singleton is None and args is None:
raise RuntimeError("Uninitialized use of ArgumentsSingleton")
if _arguments_singleton is None:
if _arguments_singleton is not None and args is not None:
raise RuntimeError("Arguments already initialized")
if args is not None:
_arguments_singleton = args
return _arguments_singleton
@ -50,7 +83,27 @@ def ApiResultsSingleton(res: ApiResults = None) -> Optional[ApiResults]:
global _api_results_singleton
if _api_results_singleton is None and res is None:
raise RuntimeError("Uninitialized use of ApiResultsSingleton")
if _api_results_singleton is None:
if _api_results_singleton is not None and res is not None:
raise RuntimeError("ApiResults already initialized")
if res is not None:
_api_results_singleton = res
return _api_results_singleton
def clear_singleton_instance(instance: Union[LegendaryCore, GlobalSignals, Namespace, ApiResults]):
global _legendary_core_singleton, _global_signals_singleton, _arguments_singleton, _api_results_singleton
if isinstance(instance, LegendaryCore):
del instance
_legendary_core_singleton = None
elif isinstance(instance, GlobalSignals):
instance.deleteLater()
del instance
_global_signals_singleton = None
elif isinstance(instance, Namespace):
del instance
_arguments_singleton = None
elif isinstance(instance, ApiResults):
del instance
_api_results_singleton = None
else:
raise RuntimeError(f"Instance is of unknown type \"{type(instance)}\"")

View file

@ -54,7 +54,6 @@ class IndicatorReasons:
class IndicatorLineEdit(QWidget):
textChanged = pyqtSignal(str)
is_valid = False
reasons = IndicatorReasons()
def __init__(
@ -97,9 +96,10 @@ class IndicatorLineEdit(QWidget):
layout.addWidget(self.indicator_label)
if not placeholder:
_translate = QCoreApplication.translate
_translate = QCoreApplication.instance().translate
self.line_edit.setPlaceholderText(_translate(self.__class__.__name__, "Default"))
self.is_valid = False
self.edit_func = edit_func
self.save_func = save_func
self.line_edit.textChanged.connect(self.__edit)
@ -107,7 +107,7 @@ class IndicatorLineEdit(QWidget):
self.line_edit.textChanged.connect(self.__save)
# lk: this can be placed here to trigger __edit
# lk: it going to save the input again if it is valid which
# lk: it is going to save the input again if it is valid which
# lk: is ok to do given the checks don't misbehave (they shouldn't)
# lk: however it is going to edit any "understood" bad input to good input
# lk: and we might not want that (but the validity check reports on the edited string)
@ -185,9 +185,6 @@ class PathEditIconProvider(QFileIconProvider):
class PathEdit(IndicatorLineEdit):
completer = QCompleter()
compl_model = QFileSystemModel()
def __init__(
self,
path: str = "",
@ -200,6 +197,9 @@ class PathEdit(IndicatorLineEdit):
horiz_policy: QSizePolicy = QSizePolicy.Expanding,
parent=None,
):
self.completer = QCompleter()
self.compl_model = QFileSystemModel()
try:
self.compl_model.setOptions(
QFileSystemModel.DontWatchForChanges
@ -230,7 +230,7 @@ class PathEdit(IndicatorLineEdit):
layout = self.layout()
layout.addWidget(self.path_select)
_translate = QCoreApplication.translate
_translate = QCoreApplication.instance().translate
self.path_select.setText(_translate("PathEdit", "Browse..."))
self.type_filter = type_filter
@ -414,7 +414,7 @@ class SelectViewWidget(QWidget):
class ImageLabel(QLabel):
image = None
img_size = None
name = str()
name = ""
def __init__(self):
super(ImageLabel, self).__init__()