Merge pull request #4070 from Ultimaker/CURA-5384_confirm_upon_exit

CURA-5384 confirm upon exit
This commit is contained in:
alekseisasin 2018-07-13 11:54:14 +02:00 committed by GitHub
commit 31891e631c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 92 deletions

View File

@ -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

View File

@ -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()

View File

View File

@ -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

View File

@ -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

View File

@ -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