diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index f6a932546a..a2ef85f3de 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -5,6 +5,7 @@ import copy import os import sys import time +from typing import cast, TYPE_CHECKING, Optional import numpy @@ -13,8 +14,6 @@ from PyQt5.QtGui import QColor, QIcon from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType -from typing import cast, TYPE_CHECKING - from UM.Scene.SceneNode import SceneNode from UM.Scene.Camera import Camera from UM.Math.Vector import Vector @@ -97,6 +96,8 @@ from . import CuraSplashScreen from . import CameraImageProvider from . import MachineActionManager +from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager + from cura.Settings.MachineManager import MachineManager from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.UserChangesModel import UserChangesModel @@ -158,6 +159,8 @@ class CuraApplication(QtApplication): self._boot_loading_time = time.time() + self._on_exit_callback_manager = OnExitCallbackManager(self) + # Variables set from CLI self._files_to_open = [] self._use_single_instance = False @@ -520,8 +523,8 @@ class CuraApplication(QtApplication): def setNeedToShowUserAgreement(self, set_value = True): self._need_to_show_user_agreement = set_value - ## The "Quit" button click event handler. - @pyqtSlot() + # DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform + # pre-exit checks such as checking for in-progress USB printing, etc. def closeApplication(self): Logger.log("i", "Close application") main_window = self.getMainWindow() @@ -530,6 +533,32 @@ class CuraApplication(QtApplication): else: self.exit(0) + # This function first performs all upon-exit checks such as USB printing that is in progress. + # Use this to close the application. + @pyqtSlot() + def checkAndExitApplication(self) -> None: + self._on_exit_callback_manager.resetCurrentState() + self._on_exit_callback_manager.triggerNextCallback() + + @pyqtSlot(result = bool) + def getIsAllChecksPassed(self) -> bool: + return self._on_exit_callback_manager.getIsAllChecksPassed() + + def getOnExitCallbackManager(self) -> "OnExitCallbackManager": + return self._on_exit_callback_manager + + def triggerNextExitCheck(self) -> None: + self._on_exit_callback_manager.triggerNextCallback() + + showConfirmExitDialog = pyqtSignal(str, arguments = ["message"]) + + def setConfirmExitDialogCallback(self, callback): + self._confirm_exit_dialog_callback = callback + + @pyqtSlot(bool) + def callConfirmExitDialogCallback(self, yes_or_no: bool): + self._confirm_exit_dialog_callback(yes_or_no) + ## Signal to connect preferences action in QML showPreferencesWindow = pyqtSignal() diff --git a/cura/TaskManagement/OnExitCallbackManager.py b/cura/TaskManagement/OnExitCallbackManager.py new file mode 100644 index 0000000000..9e641a1778 --- /dev/null +++ b/cura/TaskManagement/OnExitCallbackManager.py @@ -0,0 +1,69 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import TYPE_CHECKING, List + +from UM.Logger import Logger + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + + +# +# This class manages a all registered upon-exit checks that need to be perform when the application tries to exit. +# For example, to show a confirmation dialog when there is USB printing in progress, etc. All callbacks will be called +# in the order of when they got registered. If all callbacks "passes", that is, for example, if the user clicks "yes" +# on the exit confirmation dialog or nothing that's blocking the exit, then the application will quit after that. +# +class OnExitCallbackManager: + + def __init__(self, application: "CuraApplication") -> None: + self._application = application + self._on_exit_callback_list = list() # type: List[callable] + self._current_callback_idx = 0 + self._is_all_checks_passed = False + + def addCallback(self, callback: callable) -> None: + self._on_exit_callback_list.append(callback) + Logger.log("d", "on-app-exit callback [%s] added.", callback) + + # Reset the current state so the next time it will call all the callbacks again. + def resetCurrentState(self) -> None: + self._current_callback_idx = 0 + self._is_all_checks_passed = False + + def getIsAllChecksPassed(self) -> bool: + return self._is_all_checks_passed + + # Trigger the next callback if available. If not, it means that all callbacks have "passed", which means we should + # not block the application to quit, and it will call the application to actually quit. + def triggerNextCallback(self) -> None: + # Get the next callback and schedule that if + this_callback = None + if self._current_callback_idx < len(self._on_exit_callback_list): + this_callback = self._on_exit_callback_list[self._current_callback_idx] + self._current_callback_idx += 1 + + if this_callback is not None: + Logger.log("d", "Scheduled the next on-app-exit callback [%s]", this_callback) + self._application.callLater(this_callback) + else: + Logger.log("d", "No more on-app-exit callbacks to process. Tell the app to exit.") + + self._is_all_checks_passed = True + + # Tell the application to exit + self._application.callLater(self._application.closeApplication) + + # This is the callback function which an on-exit callback should call when it finishes, it should provide the + # "should_proceed" flag indicating whether this check has "passed", or in other words, whether quiting the + # application should be blocked. If the last on-exit callback doesn't block the quiting, it will call the next + # registered on-exit callback if available. + def onCurrentCallbackFinished(self, should_proceed: bool = True) -> None: + if not should_proceed: + Logger.log("d", "on-app-exit callback finished and we should not proceed.") + # Reset the state + self.resetCurrentState() + return + + self.triggerNextCallback() diff --git a/cura/TaskManagement/__init__.py b/cura/TaskManagement/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index f9c6011f7b..ddb215d882 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -88,6 +88,25 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._command_received = Event() self._command_received.set() + CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit) + + # This is a callback function that checks if there is any printing in progress via USB when the application tries + # to exit. If so, it will show a confirmation before + def _checkActivePrintingUponAppExit(self) -> None: + application = CuraApplication.getInstance() + if not self._is_printing: + # This USB printer is not printing, so we have nothing to do. Call the next callback if exists. + application.triggerNextExitCheck() + return + + application.setConfirmExitDialogCallback(self._onConfirmExitDialogResult) + application.showConfirmExitDialog.emit("USB printing is in progress") + + def _onConfirmExitDialogResult(self, result: bool) -> None: + if result: + application = CuraApplication.getInstance() + application.triggerNextExitCheck() + ## Reset USB device settings # def resetDeviceSettings(self): diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index d187f34122..2ad0c75cbe 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -1,11 +1,11 @@ // Copyright (c) 2017 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 -import QtQuick.Controls 1.1 -import QtQuick.Controls.Styles 1.1 +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 +import QtQuick.Dialogs 1.2 import UM 1.3 as UM import Cura 1.0 as Cura @@ -700,10 +700,42 @@ UM.MainWindow id: contextMenu } + onPreClosing: + { + close.accepted = CuraApplication.getIsAllChecksPassed(); + if (!close.accepted) + { + CuraApplication.checkAndExitApplication(); + } + } + + MessageDialog + { + id: exitConfirmationDialog + title: catalog.i18nc("@title:window", "Closing Cura") + text: catalog.i18nc("@label", "Are you sure you want to exit Cura?") + icon: StandardIcon.Question + modality: Qt.ApplicationModal + standardButtons: StandardButton.Yes | StandardButton.No + onYes: CuraApplication.callConfirmExitDialogCallback(true) + onNo: CuraApplication.callConfirmExitDialogCallback(false) + onRejected: CuraApplication.callConfirmExitDialogCallback(false) + } + + Connections + { + target: CuraApplication + onShowConfirmExitDialog: + { + exitConfirmationDialog.text = message; + exitConfirmationDialog.open(); + } + } + Connections { target: Cura.Actions.quit - onTriggered: CuraApplication.closeApplication(); + onTriggered: CuraApplication.exitApplication(); } Connections