mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-14 16:15:52 +08:00
Merge pull request #4070 from Ultimaker/CURA-5384_confirm_upon_exit
CURA-5384 confirm upon exit
This commit is contained in:
commit
31891e631c
@ -5,6 +5,7 @@ import copy
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from typing import cast, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
@ -13,8 +14,6 @@ from PyQt5.QtGui import QColor, QIcon
|
|||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||||
|
|
||||||
from typing import cast, TYPE_CHECKING
|
|
||||||
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Camera import Camera
|
from UM.Scene.Camera import Camera
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
@ -97,6 +96,8 @@ from . import CuraSplashScreen
|
|||||||
from . import CameraImageProvider
|
from . import CameraImageProvider
|
||||||
from . import MachineActionManager
|
from . import MachineActionManager
|
||||||
|
|
||||||
|
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||||
|
|
||||||
from cura.Settings.MachineManager import MachineManager
|
from cura.Settings.MachineManager import MachineManager
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Settings.UserChangesModel import UserChangesModel
|
from cura.Settings.UserChangesModel import UserChangesModel
|
||||||
@ -158,6 +159,8 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
self._boot_loading_time = time.time()
|
self._boot_loading_time = time.time()
|
||||||
|
|
||||||
|
self._on_exit_callback_manager = OnExitCallbackManager(self)
|
||||||
|
|
||||||
# Variables set from CLI
|
# Variables set from CLI
|
||||||
self._files_to_open = []
|
self._files_to_open = []
|
||||||
self._use_single_instance = False
|
self._use_single_instance = False
|
||||||
@ -522,8 +525,8 @@ class CuraApplication(QtApplication):
|
|||||||
def setNeedToShowUserAgreement(self, set_value = True):
|
def setNeedToShowUserAgreement(self, set_value = True):
|
||||||
self._need_to_show_user_agreement = set_value
|
self._need_to_show_user_agreement = set_value
|
||||||
|
|
||||||
## The "Quit" button click event handler.
|
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
|
||||||
@pyqtSlot()
|
# pre-exit checks such as checking for in-progress USB printing, etc.
|
||||||
def closeApplication(self):
|
def closeApplication(self):
|
||||||
Logger.log("i", "Close application")
|
Logger.log("i", "Close application")
|
||||||
main_window = self.getMainWindow()
|
main_window = self.getMainWindow()
|
||||||
@ -532,6 +535,32 @@ class CuraApplication(QtApplication):
|
|||||||
else:
|
else:
|
||||||
self.exit(0)
|
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
|
## Signal to connect preferences action in QML
|
||||||
showPreferencesWindow = pyqtSignal()
|
showPreferencesWindow = pyqtSignal()
|
||||||
|
|
||||||
@ -1703,22 +1732,3 @@ class CuraApplication(QtApplication):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def showMoreInformationDialogForAnonymousDataCollection(self):
|
def showMoreInformationDialogForAnonymousDataCollection(self):
|
||||||
cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")).showMoreInfoDialog()
|
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
|
|
||||||
|
69
cura/TaskManagement/OnExitCallbackManager.py
Normal file
69
cura/TaskManagement/OnExitCallbackManager.py
Normal 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()
|
0
cura/TaskManagement/__init__.py
Normal file
0
cura/TaskManagement/__init__.py
Normal file
@ -88,6 +88,25 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
self._command_received = Event()
|
self._command_received = Event()
|
||||||
self._command_received.set()
|
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
|
## Reset USB device settings
|
||||||
#
|
#
|
||||||
def resetDeviceSettings(self):
|
def resetDeviceSettings(self):
|
||||||
@ -435,9 +454,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
|
|
||||||
self._gcode_position += 1
|
self._gcode_position += 1
|
||||||
|
|
||||||
def getIsPrinting(self)-> bool:
|
|
||||||
return self._is_printing
|
|
||||||
|
|
||||||
|
|
||||||
class FirmwareUpdateState(IntEnum):
|
class FirmwareUpdateState(IntEnum):
|
||||||
idle = 0
|
idle = 0
|
||||||
|
@ -6,8 +6,7 @@ import platform
|
|||||||
import time
|
import time
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QCoreApplication
|
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
@ -51,11 +50,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
|||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self.updateUSBPrinterOutputDevices)
|
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
|
# The method updates/reset the USB settings for all connected USB devices
|
||||||
def updateUSBPrinterOutputDevices(self):
|
def updateUSBPrinterOutputDevices(self):
|
||||||
for key, device in self._usb_output_devices.items():
|
for key, device in self._usb_output_devices.items():
|
||||||
@ -190,51 +184,3 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager":
|
def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager":
|
||||||
return cls.__instance
|
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
|
|
@ -1,11 +1,11 @@
|
|||||||
// Copyright (c) 2017 Ultimaker B.V.
|
// Copyright (c) 2017 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.7
|
||||||
import QtQuick.Controls 1.1
|
import QtQuick.Controls 1.4
|
||||||
import QtQuick.Controls.Styles 1.1
|
import QtQuick.Controls.Styles 1.4
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
import QtQuick.Dialogs 1.1
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
import UM 1.3 as UM
|
import UM 1.3 as UM
|
||||||
import Cura 1.0 as Cura
|
import Cura 1.0 as Cura
|
||||||
@ -22,12 +22,6 @@ UM.MainWindow
|
|||||||
|
|
||||||
backgroundColor: UM.Theme.getColor("viewport_background")
|
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
|
// 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.
|
// It should be phased out in newer plugin versions.
|
||||||
Connections
|
Connections
|
||||||
@ -706,10 +700,50 @@ UM.MainWindow
|
|||||||
id: contextMenu
|
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
|
Connections
|
||||||
{
|
{
|
||||||
target: Cura.Actions.quit
|
target: Cura.Actions.quit
|
||||||
onTriggered: CuraApplication.closeApplication();
|
onTriggered: CuraApplication.exitApplication();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections
|
Connections
|
||||||
|
Loading…
x
Reference in New Issue
Block a user