diff --git a/rare/components/__init__.py b/rare/components/__init__.py index 684a1133..e120412f 100644 --- a/rare/components/__init__.py +++ b/rare/components/__init__.py @@ -76,14 +76,15 @@ class Rare(RareApp): @pyqtSlot() def launch_app(self): self.launch_dialog = LaunchDialog(parent=None) - self.launch_dialog.exit_app.connect(self.launch_dialog.close) - self.launch_dialog.exit_app.connect(self.__on_exit_app) - self.launch_dialog.start_app.connect(self.start_app) - self.launch_dialog.start_app.connect(self.launch_dialog.close) + self.launch_dialog.rejected.connect(self.__on_exit_app) + # lk: the reason we use the `start_app` signal here instead of accepted, is to keep the dialog + # until the main window has been created, then we call `accept()` to close the dialog + self.launch_dialog.start_app.connect(self.__on_start_app) + self.launch_dialog.start_app.connect(self.launch_dialog.accept) self.launch_dialog.login() @pyqtSlot() - def start_app(self): + def __on_start_app(self): self.timer = QTimer() self.timer.timeout.connect(self.re_login) self.poke_timer() diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index d231625b..e63d7a1b 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -1,25 +1,24 @@ -import platform from logging import getLogger from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtWidgets import QDialog, QApplication from requests.exceptions import ConnectionError, HTTPError from rare.components.dialogs.login import LoginDialog from rare.shared import RareCore from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog +from rare.widgets.dialogs import BaseDialog from rare.widgets.elide_label import ElideLabel logger = getLogger("LaunchDialog") -class LaunchDialog(QDialog): - exit_app = pyqtSignal(int) +class LaunchDialog(BaseDialog): + # lk: the reason we use the `start_app` signal here instead of accepted, is to keep the dialog + # until the main window has been created, then we call `accept()` to close the dialog start_app = pyqtSignal() def __init__(self, parent=None): super(LaunchDialog, self).__init__(parent=parent) - self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags( Qt.Window | Qt.Dialog @@ -29,12 +28,10 @@ class LaunchDialog(QDialog): | Qt.WindowMinimizeButtonHint | Qt.MSWindowsFixedSizeDialogHint ) - self.setWindowModality(Qt.WindowModal) + self.ui = Ui_LaunchDialog() self.ui.setupUi(self) - self.reject_close = True - self.progress_info = ElideLabel(parent=self) self.progress_info.setFixedHeight(False) self.ui.launch_layout.addWidget(self.progress_info) @@ -46,9 +43,11 @@ class LaunchDialog(QDialog): self.args = self.rcore.args() self.login_dialog = LoginDialog(core=self.core, parent=parent) + self.login_dialog.rejected.connect(self.reject) + self.login_dialog.accepted.connect(self.do_launch) def login(self): - do_launch = True + can_launch = True try: if self.args.offline: pass @@ -56,25 +55,29 @@ class LaunchDialog(QDialog): # Force an update check and notice in case there are API changes # self.core.check_for_updates(force=True) # self.core.force_show_update = True - if self.core.login(): + if self.core.login(force_refresh=True): logger.info("You are logged in") + self.login_dialog.close() else: - raise ValueError("You are not logged in. Open Login Window") + raise ValueError("You are not logged in. Opening login window.") except ValueError as e: logger.info(str(e)) # Do not set parent, because it won't show a task bar icon # Update: Inherit the same parent as LaunchDialog - do_launch = self.login_dialog.login() + can_launch = False + self.login_dialog.open() except (HTTPError, ConnectionError) as e: logger.warning(e) self.args.offline = True finally: - if do_launch: - if not self.args.silent: - self.show() - self.launch() - else: - self.exit_app.emit(0) + if can_launch: + self.do_launch() + + @pyqtSlot() + def do_launch(self): + if not self.args.silent: + self.open() + self.launch() def launch(self): self.progress_info.setText(self.tr("Preparing Rare")) @@ -87,9 +90,4 @@ class LaunchDialog(QDialog): def __on_completed(self): logger.info("App starting") - self.reject_close = False self.start_app.emit() - - def reject(self) -> None: - if not self.reject_close: - super(LaunchDialog, self).reject() diff --git a/rare/components/dialogs/login/__init__.py b/rare/components/dialogs/login/__init__.py index 42ba6452..5c2158e7 100644 --- a/rare/components/dialogs/login/__init__.py +++ b/rare/components/dialogs/login/__init__.py @@ -1,12 +1,14 @@ from logging import getLogger -from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtWidgets import QLayout, QDialog, QMessageBox, QFrame +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QLayout, QMessageBox, QFrame from legendary.core import LegendaryCore from rare.shared import ArgumentsSingleton from rare.ui.components.dialogs.login.landing_page import Ui_LandingPage from rare.ui.components.dialogs.login.login_dialog import Ui_LoginDialog +from rare.utils.misc import icon +from rare.widgets.dialogs import BaseDialog from rare.widgets.sliding_stack import SlidingStackedWidget from .browser_login import BrowserLogin from .import_login import ImportLogin @@ -22,12 +24,10 @@ class LandingPage(QFrame): self.ui.setupUi(self) -class LoginDialog(QDialog): - exit_app: pyqtSignal = pyqtSignal(int) +class LoginDialog(BaseDialog): def __init__(self, core: LegendaryCore, parent=None): super(LoginDialog, self).__init__(parent=parent) - self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags( Qt.Window | Qt.Dialog @@ -38,7 +38,7 @@ class LoginDialog(QDialog): | Qt.WindowCloseButtonHint | Qt.MSWindowsFixedSizeDialogHint ) - self.setWindowModality(Qt.WindowModal) + self.ui = Ui_LoginDialog() self.ui.setupUi(self) @@ -93,13 +93,22 @@ class LoginDialog(QDialog): self.landing_page.ui.login_import_radio.clicked.connect(lambda: self.ui.next_button.setEnabled(True)) self.landing_page.ui.login_import_radio.clicked.connect(self.import_radio_clicked) - self.ui.exit_button.clicked.connect(self.close) + self.ui.exit_button.clicked.connect(self.reject) self.ui.back_button.clicked.connect(self.back_clicked) self.ui.next_button.clicked.connect(self.next_clicked) self.login_stack.setCurrentWidget(self.landing_page) - self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.ui.exit_button.setIcon(icon("fa.remove")) + self.ui.back_button.setIcon(icon("fa.chevron-left")) + self.ui.next_button.setIcon(icon("fa.chevron-right")) + + # lk: Set next as the default button only to stop closing the dialog when pressing enter + self.ui.exit_button.setAutoDefault(False) + self.ui.back_button.setAutoDefault(False) + self.ui.next_button.setAutoDefault(True) + + self.ui.main_layout.setSizeConstraint(QLayout.SetFixedSize) def back_clicked(self): self.ui.back_button.setEnabled(False) @@ -129,15 +138,14 @@ class LoginDialog(QDialog): def login(self): if self.args.test_start: - return False - self.exec_() - return self.logged_in + self.reject() + self.open() def login_successful(self): try: if self.core.login(): self.logged_in = True - self.close() + self.accept() else: raise ValueError("Login failed.") except Exception as e: @@ -146,3 +154,4 @@ class LoginDialog(QDialog): self.ui.next_button.setEnabled(False) self.logged_in = False QMessageBox.warning(None, self.tr("Login error"), str(e)) + diff --git a/rare/ui/components/dialogs/launch_dialog.py b/rare/ui/components/dialogs/launch_dialog.py index 9c30344e..0808126f 100644 --- a/rare/ui/components/dialogs/launch_dialog.py +++ b/rare/ui/components/dialogs/launch_dialog.py @@ -36,7 +36,7 @@ class Ui_LaunchDialog(object): def retranslateUi(self, LaunchDialog): _translate = QtCore.QCoreApplication.translate - LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching - Rare")) + LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching")) self.title_label.setText(_translate("LaunchDialog", "

Launching Rare

")) diff --git a/rare/ui/components/dialogs/launch_dialog.ui b/rare/ui/components/dialogs/launch_dialog.ui index b8a5838e..ff9f6d34 100644 --- a/rare/ui/components/dialogs/launch_dialog.ui +++ b/rare/ui/components/dialogs/launch_dialog.ui @@ -29,7 +29,7 @@ - Launching - Rare + Launching diff --git a/rare/ui/components/dialogs/login/login_dialog.py b/rare/ui/components/dialogs/login/login_dialog.py index 76eb3310..b158a420 100644 --- a/rare/ui/components/dialogs/login/login_dialog.py +++ b/rare/ui/components/dialogs/login/login_dialog.py @@ -15,38 +15,38 @@ class Ui_LoginDialog(object): def setupUi(self, LoginDialog): LoginDialog.setObjectName("LoginDialog") LoginDialog.resize(241, 128) - self.login_layout = QtWidgets.QVBoxLayout(LoginDialog) - self.login_layout.setObjectName("login_layout") + self.main_layout = QtWidgets.QVBoxLayout(LoginDialog) + self.main_layout.setObjectName("main_layout") spacerItem = QtWidgets.QSpacerItem(0, 17, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - self.login_layout.addItem(spacerItem) + self.main_layout.addItem(spacerItem) self.welcome_label = QtWidgets.QLabel(LoginDialog) self.welcome_label.setObjectName("welcome_label") - self.login_layout.addWidget(self.welcome_label) + self.main_layout.addWidget(self.welcome_label) spacerItem1 = QtWidgets.QSpacerItem(0, 17, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - self.login_layout.addItem(spacerItem1) + self.main_layout.addItem(spacerItem1) self.login_stack_layout = QtWidgets.QVBoxLayout() self.login_stack_layout.setObjectName("login_stack_layout") - self.login_layout.addLayout(self.login_stack_layout) + self.main_layout.addLayout(self.login_stack_layout) self.button_layout = QtWidgets.QHBoxLayout() self.button_layout.setObjectName("button_layout") - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.button_layout.addItem(spacerItem2) self.exit_button = QtWidgets.QPushButton(LoginDialog) self.exit_button.setObjectName("exit_button") self.button_layout.addWidget(self.exit_button) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.button_layout.addItem(spacerItem2) self.back_button = QtWidgets.QPushButton(LoginDialog) self.back_button.setObjectName("back_button") self.button_layout.addWidget(self.back_button) self.next_button = QtWidgets.QPushButton(LoginDialog) self.next_button.setObjectName("next_button") self.button_layout.addWidget(self.next_button) - self.login_layout.addLayout(self.button_layout) + self.main_layout.addLayout(self.button_layout) self.retranslateUi(LoginDialog) def retranslateUi(self, LoginDialog): _translate = QtCore.QCoreApplication.translate - LoginDialog.setWindowTitle(_translate("LoginDialog", "Login - Rare")) + LoginDialog.setWindowTitle(_translate("LoginDialog", "Login")) self.welcome_label.setText(_translate("LoginDialog", "

Welcome to Rare

")) self.exit_button.setText(_translate("LoginDialog", "Exit")) self.back_button.setText(_translate("LoginDialog", "Back")) diff --git a/rare/ui/components/dialogs/login/login_dialog.ui b/rare/ui/components/dialogs/login/login_dialog.ui index 5419629b..6ca92710 100644 --- a/rare/ui/components/dialogs/login/login_dialog.ui +++ b/rare/ui/components/dialogs/login/login_dialog.ui @@ -11,9 +11,9 @@ - Login - Rare + Login - + @@ -58,6 +58,13 @@ + + + + Exit + + + @@ -71,13 +78,6 @@ - - - - Exit - - - diff --git a/rare/widgets/dialogs.py b/rare/widgets/dialogs.py new file mode 100644 index 00000000..a5a97db8 --- /dev/null +++ b/rare/widgets/dialogs.py @@ -0,0 +1,283 @@ +import sys +from abc import abstractmethod + +from PyQt5.QtCore import Qt, pyqtSlot, QCoreApplication, QSize +from PyQt5.QtGui import QCloseEvent, QKeyEvent, QKeySequence +from PyQt5.QtWidgets import ( + QDialog, + QDialogButtonBox, + QApplication, + QPushButton, + QVBoxLayout, + QHBoxLayout, + QWidget, + QLayout, QSpacerItem, QSizePolicy, +) + +from rare.utils.misc import icon + + +def dialog_title_game(text: str, app_title: str) -> str: + return f"{text} '{app_title}'" + + +def dialog_title(text: str) -> str: + return f"{text} - {QCoreApplication.instance().applicationName()}" + + +class BaseDialog(QDialog): + + def __init__(self, parent=None): + super(BaseDialog, self).__init__(parent=parent) + self.setAttribute(Qt.WA_DeleteOnClose, True) + self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) + self.setWindowModality(Qt.WindowModal) + + def setWindowTitle(self, a0): + super().setWindowTitle(dialog_title(a0)) + + def exec(self): + raise RuntimeError(f"Don't use `exec()` with {type(self).__name__}") + + def exec_(self): + raise RuntimeError(f"Don't use `exec_()` with {type(self).__name__}") + + # lk: because you will eventually find yourself back here. + # on QDialogs the Esc key closes the dialog through keyPressEvent(), + # which ultimately call `reject()`. Pressing the Enter/Return button + # is a shortcut for pressing the default button and thus calling `accept()` + # In turn both `accept()` and `reject()` evetually call `done()`. + + # In the base dialog ignore both. In the subclasses, call the method + # from QDialog if required, not this one. + # `super(BaseDialog, self).keyPressEvent(a0)` + def keyPressEvent(self, a0: QKeyEvent) -> None: + if a0.matches(QKeySequence.Cancel): + a0.ignore() + return + if a0.key() == Qt.Key_Enter or a0.key() == Qt.Key_Return: + a0.ignore() + return + super().keyPressEvent(a0) + + # Using the 'X' button on the window manager comes directly here. + # It is a spontaneous event so simply ignore it. + def closeEvent(self, a0: QCloseEvent) -> None: + if a0.spontaneous(): + a0.ignore() + return + super().closeEvent(a0) + + +class ButtonDialog(BaseDialog): + + def __init__(self, parent=None): + super(ButtonDialog, self).__init__(parent=parent) + + self.reject_button = QPushButton(self) + self.reject_button.setText(self.tr("Cancel")) + self.reject_button.setIcon(icon("fa.remove")) + self.reject_button.setAutoDefault(False) + self.reject_button.clicked.connect(self.reject) + + self.accept_button = QPushButton(self) + self.accept_button.setAutoDefault(False) + self.accept_button.clicked.connect(self.accept) + + self.button_layout = QHBoxLayout() + self.button_layout.addWidget(self.reject_button) + self.button_layout.addStretch(20) + self.button_layout.addStretch(1) + self.button_layout.addWidget(self.accept_button) + + self.main_layout = QVBoxLayout(self) + # lk: dirty way to set a minimum width with fixed size constraint + spacer = QSpacerItem( + 480, self.main_layout.spacing(), + QSizePolicy.Expanding, QSizePolicy.Fixed + ) + self.main_layout.addItem(spacer) + self.main_layout.addLayout(self.button_layout) + self.main_layout.setSizeConstraint(QLayout.SetFixedSize) + + def close(self): + raise RuntimeError(f"Don't use `close()` with {type(self).__name__}") + + def setCentralWidget(self, widget: QWidget): + widget.layout().setContentsMargins(0, 0, 0, 0) + self.main_layout.insertWidget(0, widget) + + def setCentralLayout(self, layout: QLayout): + layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.insertLayout(0, layout) + + @abstractmethod + def accept_handler(self): + raise NotImplementedError + + @abstractmethod + def reject_handler(self): + raise NotImplementedError + + @abstractmethod + def done_handler(self): + raise NotImplementedError + + # These only apply to QDialog. If we move to QWidget for embedded dialogs + # we have to use close() and custom handling. + + # lk: Override accept to run our abstract handling method + def accept(self): + self.accept_handler() + super().accept() + + # lk: Override reject to run our abstract handling method + def reject(self): + self.reject_handler() + super().reject() + + # lk: Override `done()` to to run our abstract handling method + def done(self, a0): + self.done_handler() + super().done(a0) + + # lk: Ignore BaseDialog::keyPressEvent and call QDialog::keyPressEvent + # because we handle accept and reject here. + def keyPressEvent(self, a0: QKeyEvent) -> None: + super(BaseDialog, self).keyPressEvent(a0) + + # lk: Ignore BaseDialog::closeEvent and call QDialog::closeEvent + # because we handle accept and reject here. + def closeEvent(self, a0: QCloseEvent) -> None: + super(BaseDialog, self).closeEvent(a0) + + +class ActionDialog(ButtonDialog): + def __init__(self, parent=None): + super(ActionDialog, self).__init__(parent=parent) + self.__reject_close = False + + self.action_button = QPushButton(self) + self.action_button.setAutoDefault(True) + self.action_button.clicked.connect(self.action) + + self.button_layout.insertWidget(2, self.action_button) + + def active(self) -> bool: + return self.__reject_close + + def setActive(self, active: bool): + self.reject_button.setDisabled(active) + self.action_button.setDisabled(active) + self.accept_button.setDisabled(active) + self.__reject_close = active + + @abstractmethod + def action_handler(self): + raise NotImplementedError + + @pyqtSlot() + def action(self): + self.setActive(True) + self.action_handler() + + # lk: Ignore all key presses if there is an ongoing action + def keyPressEvent(self, a0: QKeyEvent) -> None: + if self.__reject_close: + a0.ignore() + return + super(BaseDialog, self).keyPressEvent(a0) + + # lk: Ignore all closeEvents if there is an ongoing action + def closeEvent(self, a0: QCloseEvent) -> None: + if self.__reject_close: + a0.ignore() + return + super(BaseDialog, self).closeEvent(a0) + + +__all__ = ["dialog_title", "dialog_title_game", "BaseDialog", "ButtonDialog", "ActionDialog"] + + +class TestDialog(BaseDialog): + def __init__(self, parent=None): + super(TestDialog, self).__init__(parent=parent) + + self.accept_button = QPushButton("accept", self) + self.reject_button = QPushButton("reject", self) + self.action_button = QPushButton("action", self) + self.button_box = QDialogButtonBox(Qt.Horizontal, self) + self.button_box.addButton(self.accept_button, QDialogButtonBox.AcceptRole) + self.button_box.addButton(self.reject_button, QDialogButtonBox.RejectRole) + self.button_box.addButton(self.action_button, QDialogButtonBox.ActionRole) + + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + layout = QVBoxLayout(self) + layout.addWidget(self.button_box) + + self.setMinimumWidth(480) + + def setWindowTitle(self, a0): + super().setWindowTitle(dialog_title(a0)) + + def close(self): + print("in close") + super().close() + + def closeEvent(self, a0: QCloseEvent) -> None: + print("in closeEvent") + if a0.spontaneous(): + print("is spontaneous") + a0.ignore() + return + if self.reject_close: + a0.ignore() + else: + self._on_close() + super().closeEvent(a0) + # super().closeEvent(a0) + + def done(self, a0): + print(f"in done {a0}") + return + super().done(a0) + + def accept(self): + print("in accept") + self._on_accept() + # return + # super().accept() + + def reject(self): + print("in reject") + self._on_reject() + # return + # super().reject() + + def _on_close(self): + print("in _on_close") + + def _on_accept(self): + print("in _on_accepted") + # self.close() + + def _on_reject(self): + print("in _on_rejected") + self.close() + + def keyPressEvent(self, a0: QKeyEvent) -> None: + super(BaseDialog, self).keyPressEvent(a0) + + +def test_dialog(): + app = QApplication(sys.argv) + dlg = TestDialog(None) + dlg.show() + ret = app.exec() + sys.exit(ret) + + +if __name__ == "__main__": + test_dialog()