diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 16689f05ff..087f081ae9 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 @@ -522,8 +525,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() @@ -532,6 +535,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() @@ -1703,22 +1732,3 @@ class CuraApplication(QtApplication): @pyqtSlot() def showMoreInformationDialogForAnonymousDataCollection(self): cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")).showMoreInfoDialog() - - ## Signal to check whether the application can be closed or not - checkCuraCloseChange = pyqtSignal() - - # This variable is necessary to ensure that all methods that were subscribed for the checkCuraCloseChange event - # have been passed checks - _isCuraCanBeClosed = True - - def setCuraCanBeClosed(self, value: bool): - self._isCuraCanBeClosed = value - - @pyqtSlot(result=bool) - def preCloseEventHandler(self)-> bool: - - # If any of checks failed then then _isCuraCanBeClosed should be set to False and Cura will not be closed - # after clicking the quit button - self.checkCuraCloseChange.emit() - - return self._isCuraCanBeClosed diff --git a/cura/TaskManagement/OnExitCallbackManager.py b/cura/TaskManagement/OnExitCallbackManager.py new file mode 100644 index 0000000000..2e8e42595b --- /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, Callable, 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 fc9558056b..109888a15d 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(catalog.i18nc("@label", "A USB print is in progress, closing Cura will stop this print. Are you sure?")) + + def _onConfirmExitDialogResult(self, result: bool) -> None: + if result: + application = CuraApplication.getInstance() + application.triggerNextExitCheck() + ## Reset USB device settings # def resetDeviceSettings(self): @@ -435,9 +454,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._gcode_position += 1 - def getIsPrinting(self)-> bool: - return self._is_printing - class FirmwareUpdateState(IntEnum): idle = 0 diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index b2dc24480c..2ee85187ee 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -6,8 +6,7 @@ import platform import time import serial.tools.list_ports -from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QCoreApplication -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal from UM.Logger import Logger from UM.Resources import Resources @@ -51,11 +50,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._application.globalContainerStackChanged.connect(self.updateUSBPrinterOutputDevices) - self._application.checkCuraCloseChange.connect(self.checkWheterUSBIsActiveOrNot) - - self._lock = threading.Lock() - self._confirm_dialog_visible = False - # The method updates/reset the USB settings for all connected USB devices def updateUSBPrinterOutputDevices(self): for key, device in self._usb_output_devices.items(): @@ -190,51 +184,3 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): @classmethod def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager": return cls.__instance - - # The method checks whether a printer is printing via USB or not before closing cura. If the printer is printing then pop up a - # dialog to confirm stop printing - def checkWheterUSBIsActiveOrNot(self)-> None: - - is_printing = False - for key, device in self._usb_output_devices.items(): - if type(device) is USBPrinterOutputDevice.USBPrinterOutputDevice: - if device.getIsPrinting(): - is_printing = True - break - - if is_printing: - if threading.current_thread() != threading.main_thread(): - self._lock.acquire() - self._confirm_dialog_visible = True - - CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Confirm stop printing"), - i18n_catalog.i18nc("@window:message","A USB print is in progress, closing Cura will stop this print. Are you sure?"), - buttons=QMessageBox.Yes + QMessageBox.No, - icon=QMessageBox.Question, - callback=self._messageBoxCallback) - # Wait for dialog result - self.waitForClose() - - ## Block thread until the dialog is closed. - def waitForClose(self)-> None: - if self._confirm_dialog_visible: - if threading.current_thread() != threading.main_thread(): - self._lock.acquire() - self._lock.release() - else: - # If this is not run from a separate thread, we need to ensure that the events are still processed. - while self._confirm_dialog_visible: - time.sleep(1 / 50) - QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - - def _messageBoxCallback(self, button): - if button == QMessageBox.Yes: - self._application.setCuraCanBeClosed(True) - else: - self._application.setCuraCanBeClosed(False) - - self._confirm_dialog_visible = False - try: - self._lock.release() - except: - pass \ No newline at end of file diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 7b4f3c613b..61d2c9f655 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 @@ -22,12 +22,6 @@ UM.MainWindow backgroundColor: UM.Theme.getColor("viewport_background") - // Event which does the check before closing the window - onPreCloseChange: - { - event.accepted = CuraApplication.preCloseEventHandler() - } - // This connection is here to support legacy printer output devices that use the showPrintMonitor signal on Application to switch to the monitor stage // It should be phased out in newer plugin versions. Connections @@ -706,10 +700,50 @@ 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) + onVisibilityChanged: + { + if (!visible) + { + // reset the text to default because other modules may change the message text. + text = catalog.i18nc("@label", "Are you sure you want to exit Cura?"); + } + } + } + + Connections + { + target: CuraApplication + onShowConfirmExitDialog: + { + exitConfirmationDialog.text = message; + exitConfirmationDialog.open(); + } + } + Connections { target: Cura.Actions.quit - onTriggered: CuraApplication.closeApplication(); + onTriggered: CuraApplication.exitApplication(); } Connections