mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-13 21:05:54 +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 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
|
||||
|
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.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
|
||||
|
@ -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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user