diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 7366b95f86..c194f5df32 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -12,6 +12,8 @@ if TYPE_CHECKING: from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel +from cura.PrinterOutput.ConfigurationChangeModel import ConfigurationChangeModel + class PrintJobOutputModel(QObject): stateChanged = pyqtSignal() @@ -24,6 +26,7 @@ class PrintJobOutputModel(QObject): configurationChanged = pyqtSignal() previewImageChanged = pyqtSignal() compatibleMachineFamiliesChanged = pyqtSignal() + configurationChangesChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: super().__init__(parent) @@ -41,6 +44,7 @@ class PrintJobOutputModel(QObject): self._preview_image_id = 0 self._preview_image = None # type: Optional[QImage] + self._configuration_changes = [] # type: List[ConfigurationChangeModel] @pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged) def compatibleMachineFamilies(self): @@ -147,4 +151,14 @@ class PrintJobOutputModel(QObject): @pyqtSlot(str) def setState(self, state): - self._output_controller.setJobState(self, state) \ No newline at end of file + self._output_controller.setJobState(self, state) + + @pyqtProperty("QVariantList", notify=configurationChangesChanged) + def configurationChanges(self) -> List[ConfigurationChangeModel]: + return self._configuration_changes + + def updateConfigurationChanges(self, changes: List[ConfigurationChangeModel]) -> None: + if len(self._configuration_changes) == 0 and len(changes) == 0: + return + self._configuration_changes = changes + self.configurationChangesChanged.emit() diff --git a/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml index f6cf6607c7..b55b5c6779 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml @@ -85,7 +85,6 @@ Component anchors.right: parent.right anchors.rightMargin: UM.Theme.getSize("default_margin").height anchors.leftMargin: UM.Theme.getSize("default_margin").height - height: 175 * screenScaleFactor } } } diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrintJobInfoBlock.qml b/plugins/UM3NetworkPrinting/resources/qml/PrintJobInfoBlock.qml index 71c2104318..f0e07807e2 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/PrintJobInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/PrintJobInfoBlock.qml @@ -2,6 +2,8 @@ import QtQuick 2.2 import QtQuick.Controls 2.0 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 import UM 1.3 as UM @@ -9,6 +11,110 @@ import UM 1.3 as UM Item { id: base + + function haveAlert() { + return printJob.configurationChanges.length !== 0; + } + + function alertHeight() { + return haveAlert() ? 230 : 0; + } + + function alertText() { + if (printJob.configurationChanges.length === 0) { + return ""; + } + + var topLine; + if (materialsAreKnown(printJob)) { + topLine = catalog.i18nc("@label", "The assigned printer, %1, requires the following configuration change(s):").arg(printJob.assignedPrinter.name); + } else { + topLine = catalog.i18nc("@label", "The printer %1 is assigned, but the job contains an unknown material configuration.").arg(printJob.assignedPrinter.name); + } + var result = "

" + topLine +"

"; + + for (var i=0; i

"; + } + return result; + } + + function materialsAreKnown(printJob) { + var conf0 = printJob.configuration[0]; + if (conf0 && !conf0.material.material) { + return false; + } + + var conf1 = printJob.configuration[1]; + if (conf1 && !conf1.material.material) { + return false; + } + + return true; + } + + function formatBuildPlateType(buildPlateType) { + var translationText = ""; + + switch (buildPlateType) { + case 'glass': + translationText = catalog.i18nc("@label", "Glass"); + break; + case 'aluminum': + translationText = catalog.i18nc("@label", "Aluminum"); + break; + default: + translationText = null; + } + return translationText; + } + + function formatPrintJobName(name) { + var extensionsToRemove = [ + '.gz', + '.gcode', + '.ufp' + ]; + + for (var i = 0; i < extensionsToRemove.length; i++) { + var extension = extensionsToRemove[i]; + + if (name.slice(-extension.length) === extension) { + name = name.substring(0, name.length - extension.length); + } + } + + return name; + } + + function isPrintJobForcable(printJob) { + var forcable = true; + + for (var i = 0; i < printJob.configurationChanges.length; i++) { + var typeOfChange = printJob.configurationChanges[i].typeOfChange; + if (typeOfChange === 'material_insert' || typeOfChange === 'buildplate_change') { + forcable = false; + } + } + + return forcable; + } + + property var cardHeight: 175 + + height: (cardHeight + alertHeight()) * screenScaleFactor property var printJob: null property var shadowRadius: 5 * screenScaleFactor function getPrettyTime(time) @@ -27,6 +133,9 @@ Item Rectangle { id: background + + height: (cardHeight + alertHeight()) * screenScaleFactor + anchors { top: parent.top @@ -35,7 +144,7 @@ Item leftMargin: base.shadowRadius rightMargin: base.shadowRadius right: parent.right - bottom: parent.bottom + //bottom: parent.bottom - alertHeight() * screenScaleFactor bottomMargin: base.shadowRadius } @@ -47,6 +156,18 @@ Item color: "#3F000000" // 25% shadow } + Rectangle + { + height: cardHeight * screenScaleFactor + + anchors + { + top: parent.top + left: parent.left + right: parent.right + // bottom: parent.bottom + } + Item { // Content on the left of the infobox @@ -392,15 +513,128 @@ Item } } - + } Rectangle { + height: cardHeight * screenScaleFactor color: UM.Theme.getColor("viewport_background") width: 2 * screenScaleFactor anchors.top: parent.top - anchors.bottom: parent.bottom anchors.margins: UM.Theme.getSize("default_margin").height anchors.horizontalCenter: parent.horizontalCenter } + + // Alert / Configuration change box + Rectangle + { + height: alertHeight() * screenScaleFactor + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + +color: "#ff00ff" + ColumnLayout + { + anchors.fill: parent + RowLayout + { + Item + { + Layout.fillWidth: true + } + + Label + { + font: UM.Theme.getFont("default_bold") + text: "Configuration change" + } + + UM.RecolorImage + { + id: collapseIcon + width: 15 + height: 15 + sourceSize.width: width + sourceSize.height: height + + // FIXME + source: base.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom") + color: "black" + } + + Item + { + Layout.fillWidth: true + } + + } + + Rectangle + { + Layout.fillHeight: true + Layout.fillWidth: true +color: "red" + + Rectangle + { +color: "green" + width: childrenRect.width + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: parent.bottom + + ColumnLayout + { + width: childrenRect.width + + anchors.top: parent.top + anchors.bottom: parent.bottom + + Text + { + Layout.alignment: Qt.AlignTop + + textFormat: Text.StyledText + font: UM.Theme.getFont("default_bold") + text: alertText() + } + + Button + { + visible: isPrintJobForcable(printJob) + text: catalog.i18nc("@label", "Override") + onClicked: { + overrideConfirmationDialog.visible = true; + } + } + + // Spacer + Item + { + Layout.fillHeight: true + } + } + } + } + } + } + + MessageDialog + { + id: overrideConfirmationDialog + title: catalog.i18nc("@window:title", "Override configuration") + icon: StandardIcon.Warning + text: { + var printJobName = formatPrintJobName(printJob.name); + var confirmText = catalog.i18nc("@label", "Starting a print job with an incompatible configuration could damage your 3D printer. Are you sure you want to override the configuration and print %1?").arg(printJobName); + return confirmText; + } + + standardButtons: StandardButton.Yes | StandardButton.No + Component.onCompleted: visible = false + onYes: OutputDevice.forceSendJob(printJob.key) + } } } \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py index 409ca7a84a..79040373ae 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py @@ -17,6 +17,7 @@ from UM.Scene.SceneNode import SceneNode # For typing. from UM.Version import Version # To check against firmware versions for support. from cura.CuraApplication import CuraApplication +from cura.PrinterOutput.ConfigurationChangeModel import ConfigurationChangeModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState @@ -406,6 +407,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # is a modification of the cluster queue and not of the actual job. self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None) + @pyqtSlot(str) + def forceSendJob(self, print_job_uuid: str) -> None: + data = "{\"force\": true}" + self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None) + def _printJobStateChanged(self) -> None: username = self._getUserName() @@ -574,6 +580,16 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if not status_set_by_impediment: print_job.updateState(data["status"]) + print_job.updateConfigurationChanges(self._createConfigurationChanges(data["configuration_changes_required"])) + + def _createConfigurationChanges(self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]: + result = [] + for change in data: + result.append(ConfigurationChangeModel(type_of_change=change["type_of_change"], + index=change["index"], + target_name=change["target_name"], + origin_name=change["origin_name"])) + return result def _createMaterialOutputModel(self, material_data) -> MaterialOutputModel: containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_data["guid"])