From ac3d3bc5c08671a5afee481ea0c8f27e69ba98ad Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 13 Jul 2018 08:08:56 +0200 Subject: [PATCH 1/6] Revert "Added signal to prevent window closing if USB printer is printing" This reverts commit 596a7b7169b9c7e41cd4751f99eea656f67ed2b7. --- cura/CuraApplication.py | 19 ------- plugins/USBPrinting/USBPrinterOutputDevice.py | 3 - .../USBPrinterOutputDeviceManager.py | 56 +------------------ resources/qml/Cura.qml | 6 -- 4 files changed, 1 insertion(+), 83 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 3ffa4291ba..f6a932546a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1701,22 +1701,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/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index fc9558056b..f9c6011f7b 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -435,9 +435,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..d187f34122 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -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 From c0b7e40b0d250818060b6dbaded2a6de0fd6b7e6 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 13 Jul 2018 09:16:09 +0200 Subject: [PATCH 2/6] Add on-exit callback management and check for active USB printing CURA-5384 --- cura/CuraApplication.py | 37 ++++++++-- cura/TaskManagement/OnExitCallbackManager.py | 69 +++++++++++++++++++ cura/TaskManagement/__init__.py | 0 plugins/USBPrinting/USBPrinterOutputDevice.py | 19 +++++ resources/qml/Cura.qml | 42 +++++++++-- 5 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 cura/TaskManagement/OnExitCallbackManager.py create mode 100644 cura/TaskManagement/__init__.py 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 From e6c6edc9ebaaff9b21d78d245ea4963ffaac73d9 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 13 Jul 2018 09:21:34 +0200 Subject: [PATCH 3/6] Fix code style CURA-5384 --- cura/TaskManagement/OnExitCallbackManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/TaskManagement/OnExitCallbackManager.py b/cura/TaskManagement/OnExitCallbackManager.py index 9e641a1778..2e8e42595b 100644 --- a/cura/TaskManagement/OnExitCallbackManager.py +++ b/cura/TaskManagement/OnExitCallbackManager.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Callable, List from UM.Logger import Logger @@ -19,11 +19,11 @@ class OnExitCallbackManager: def __init__(self, application: "CuraApplication") -> None: self._application = application - self._on_exit_callback_list = list() # type: List[callable] + 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: + def addCallback(self, callback: Callable) -> None: self._on_exit_callback_list.append(callback) Logger.log("d", "on-app-exit callback [%s] added.", callback) From 49bd249819924f0cf1f0881c646fc01fd102f6f9 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 13 Jul 2018 11:09:58 +0200 Subject: [PATCH 4/6] Update text for USB printing exit confirmation CURA-5384 --- plugins/USBPrinting/USBPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index ddb215d882..109888a15d 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -100,7 +100,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): return application.setConfirmExitDialogCallback(self._onConfirmExitDialogResult) - application.showConfirmExitDialog.emit("USB printing is in progress") + 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: From 6d34d77b6b2a888d03584b2b68f130af70660815 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 13 Jul 2018 11:10:38 +0200 Subject: [PATCH 5/6] Reset confirm exit dialog text upon close CURA-5384 --- resources/qml/Cura.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 2ad0c75cbe..61d2c9f655 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -720,6 +720,14 @@ UM.MainWindow 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 From 62f219757d25c52fd83df505614e8cec0ccc9f51 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Fri, 13 Jul 2018 11:47:42 +0200 Subject: [PATCH 6/6] Set machine defintion setting machine_head_with_fans_polygon for Prusa-i3 CURA-5569 --- resources/definitions/prusa_i3.def.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/definitions/prusa_i3.def.json b/resources/definitions/prusa_i3.def.json index a3759eb6c0..c676f7fe96 100644 --- a/resources/definitions/prusa_i3.def.json +++ b/resources/definitions/prusa_i3.def.json @@ -40,6 +40,14 @@ [18, -18] ] }, + "machine_head_with_fans_polygon": { + "default_value": [ + [-75, -18], + [-75, 35], + [18, 35], + [18, -18] + ] + }, "gantry_height": { "default_value": 55 },