diff --git a/cura/CameraImageProvider.py b/cura/CameraImageProvider.py index ff5c51f24b..6a07f6b029 100644 --- a/cura/CameraImageProvider.py +++ b/cura/CameraImageProvider.py @@ -19,5 +19,11 @@ class CameraImageProvider(QQuickImageProvider): return image, QSize(15, 15) except AttributeError: - pass + try: + image = output_device.activeCamera.getImage() + + return image, QSize(15, 15) + except AttributeError: + pass + return QImage(), QSize(15, 15) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 833f43e29c..dbc675a279 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -93,6 +93,7 @@ from . import CuraActions from cura.Scene import ZOffsetDecorator from . import CuraSplashScreen from . import CameraImageProvider +from . import PrintJobPreviewImageProvider from . import MachineActionManager from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager @@ -502,6 +503,7 @@ class CuraApplication(QtApplication): def _onEngineCreated(self): self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) + self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider()) @pyqtProperty(bool) def needToShowUserAgreement(self): diff --git a/cura/PrintJobPreviewImageProvider.py b/cura/PrintJobPreviewImageProvider.py new file mode 100644 index 0000000000..a8df5aa273 --- /dev/null +++ b/cura/PrintJobPreviewImageProvider.py @@ -0,0 +1,27 @@ +from PyQt5.QtGui import QImage +from PyQt5.QtQuick import QQuickImageProvider +from PyQt5.QtCore import QSize + +from UM.Application import Application + + +class PrintJobPreviewImageProvider(QQuickImageProvider): + def __init__(self): + super().__init__(QQuickImageProvider.Image) + + ## Request a new image. + def requestImage(self, id: str, size: QSize) -> QImage: + # The id will have an uuid and an increment separated by a slash. As we don't care about the value of the + # increment, we need to strip that first. + uuid = id[id.find("/") + 1:] + for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): + if not hasattr(output_device, "printJobs"): + continue + + for print_job in output_device.printJobs: + if print_job.key == uuid: + if print_job.getPreviewImage(): + return print_job.getPreviewImage(), QSize(15, 15) + else: + return QImage(), QSize(15, 15) + return QImage(), QSize(15,15) \ No newline at end of file diff --git a/cura/PrinterOutput/ConfigurationModel.py b/cura/PrinterOutput/ConfigurationModel.py index c03d968b9e..a3d6afd01d 100644 --- a/cura/PrinterOutput/ConfigurationModel.py +++ b/cura/PrinterOutput/ConfigurationModel.py @@ -27,7 +27,13 @@ class ConfigurationModel(QObject): return self._printer_type def setExtruderConfigurations(self, extruder_configurations): - self._extruder_configurations = extruder_configurations + if self._extruder_configurations != extruder_configurations: + self._extruder_configurations = extruder_configurations + + for extruder_configuration in self._extruder_configurations: + extruder_configuration.extruderConfigurationChanged.connect(self.configurationChanged) + + self.configurationChanged.emit() @pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged) def extruderConfigurations(self): diff --git a/cura/PrinterOutput/ExtruderConfigurationModel.py b/cura/PrinterOutput/ExtruderConfigurationModel.py index bc7f1a7c07..da0ad6b0b2 100644 --- a/cura/PrinterOutput/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/ExtruderConfigurationModel.py @@ -1,56 +1,67 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal +from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel + class ExtruderConfigurationModel(QObject): extruderConfigurationChanged = pyqtSignal() - def __init__(self): + def __init__(self, position: int = -1) -> None: super().__init__() - self._position = -1 - self._material = None - self._hotend_id = None + self._position = position # type: int + self._material = None # type: Optional[MaterialOutputModel] + self._hotend_id = None # type: Optional[str] - def setPosition(self, position): + def setPosition(self, position: int) -> None: self._position = position @pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged) - def position(self): + def position(self) -> int: return self._position - def setMaterial(self, material): - self._material = material + def setMaterial(self, material: Optional[MaterialOutputModel]) -> None: + if self._hotend_id != material: + self._material = material + self.extruderConfigurationChanged.emit() @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) - def material(self): + def activeMaterial(self) -> Optional[MaterialOutputModel]: return self._material - def setHotendID(self, hotend_id): - self._hotend_id = hotend_id + @pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged) + def material(self) -> Optional[MaterialOutputModel]: + return self._material + + def setHotendID(self, hotend_id: Optional[str]) -> None: + if self._hotend_id != hotend_id: + self._hotend_id = hotend_id + self.extruderConfigurationChanged.emit() @pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged) - def hotendID(self): + def hotendID(self) -> Optional[str]: return self._hotend_id ## This method is intended to indicate whether the configuration is valid or not. # The method checks if the mandatory fields are or not set # At this moment is always valid since we allow to have empty material and variants. - def isValid(self): + def isValid(self) -> bool: return True - def __str__(self): + def __str__(self) -> str: message_chunks = [] message_chunks.append("Position: " + str(self._position)) message_chunks.append("-") - message_chunks.append("Material: " + self.material.type if self.material else "empty") + message_chunks.append("Material: " + self.activeMaterial.type if self.activeMaterial else "empty") message_chunks.append("-") message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty") return " ".join(message_chunks) - def __eq__(self, other): + def __eq__(self, other) -> bool: return hash(self) == hash(other) # Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is diff --git a/cura/PrinterOutput/ExtruderOutputModel.py b/cura/PrinterOutput/ExtruderOutputModel.py index 0726662c6c..30d53bbd85 100644 --- a/cura/PrinterOutput/ExtruderOutputModel.py +++ b/cura/PrinterOutput/ExtruderOutputModel.py @@ -12,64 +12,61 @@ if TYPE_CHECKING: class ExtruderOutputModel(QObject): - hotendIDChanged = pyqtSignal() targetHotendTemperatureChanged = pyqtSignal() hotendTemperatureChanged = pyqtSignal() - activeMaterialChanged = pyqtSignal() + extruderConfigurationChanged = pyqtSignal() isPreheatingChanged = pyqtSignal() - def __init__(self, printer: "PrinterOutputModel", position, parent=None) -> None: + def __init__(self, printer: "PrinterOutputModel", position: int, parent=None) -> None: super().__init__(parent) - self._printer = printer + self._printer = printer # type: PrinterOutputModel self._position = position - self._target_hotend_temperature = 0 # type: float - self._hotend_temperature = 0 # type: float - self._hotend_id = "" - self._active_material = None # type: Optional[MaterialOutputModel] - self._extruder_configuration = ExtruderConfigurationModel() - self._extruder_configuration.position = self._position + self._target_hotend_temperature = 0.0 # type: float + self._hotend_temperature = 0.0 # type: float self._is_preheating = False - def getPrinter(self): + # The extruder output model wraps the configuration model. This way we can use the same config model for jobs + # and extruders alike. + self._extruder_configuration = ExtruderConfigurationModel() + self._extruder_configuration.position = self._position + self._extruder_configuration.extruderConfigurationChanged.connect(self.extruderConfigurationChanged) + + def getPrinter(self) -> "PrinterOutputModel": return self._printer - def getPosition(self): + def getPosition(self) -> int: return self._position # Does the printer support pre-heating the bed at all @pyqtProperty(bool, constant=True) - def canPreHeatHotends(self): + def canPreHeatHotends(self) -> bool: if self._printer: return self._printer.canPreHeatHotends return False - @pyqtProperty(QObject, notify = activeMaterialChanged) + @pyqtProperty(QObject, notify = extruderConfigurationChanged) def activeMaterial(self) -> Optional["MaterialOutputModel"]: - return self._active_material + return self._extruder_configuration.activeMaterial - def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]): - if self._active_material != material: - self._active_material = material - self._extruder_configuration.material = self._active_material - self.activeMaterialChanged.emit() - self.extruderConfigurationChanged.emit() + def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None: + self._extruder_configuration.setMaterial(material) ## Update the hotend temperature. This only changes it locally. - def updateHotendTemperature(self, temperature: float): + def updateHotendTemperature(self, temperature: float) -> None: if self._hotend_temperature != temperature: self._hotend_temperature = temperature self.hotendTemperatureChanged.emit() - def updateTargetHotendTemperature(self, temperature: float): + def updateTargetHotendTemperature(self, temperature: float) -> None: if self._target_hotend_temperature != temperature: self._target_hotend_temperature = temperature self.targetHotendTemperatureChanged.emit() ## Set the target hotend temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) - def setTargetHotendTemperature(self, temperature: float): + def setTargetHotendTemperature(self, temperature: float) -> None: self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self.updateTargetHotendTemperature(temperature) @@ -81,30 +78,26 @@ class ExtruderOutputModel(QObject): def hotendTemperature(self) -> float: return self._hotend_temperature - @pyqtProperty(str, notify = hotendIDChanged) + @pyqtProperty(str, notify = extruderConfigurationChanged) def hotendID(self) -> str: - return self._hotend_id + return self._extruder_configuration.hotendID - def updateHotendID(self, id: str): - if self._hotend_id != id: - self._hotend_id = id - self._extruder_configuration.hotendID = self._hotend_id - self.hotendIDChanged.emit() - self.extruderConfigurationChanged.emit() + def updateHotendID(self, hotend_id: str) -> None: + self._extruder_configuration.setHotendID(hotend_id) @pyqtProperty(QObject, notify = extruderConfigurationChanged) - def extruderConfiguration(self): + def extruderConfiguration(self) -> Optional[ExtruderConfigurationModel]: if self._extruder_configuration.isValid(): return self._extruder_configuration return None - def updateIsPreheating(self, pre_heating): + def updateIsPreheating(self, pre_heating: bool) -> None: if self._is_preheating != pre_heating: self._is_preheating = pre_heating self.isPreheatingChanged.emit() @pyqtProperty(bool, notify=isPreheatingChanged) - def isPreheating(self): + def isPreheating(self) -> bool: return self._is_preheating ## Pre-heats the extruder before printer. @@ -113,9 +106,9 @@ class ExtruderOutputModel(QObject): # Celsius. # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) - def preheatHotend(self, temperature, duration): + def preheatHotend(self, temperature: float, duration: float) -> None: self._printer._controller.preheatHotend(self, temperature, duration) @pyqtSlot() - def cancelPreheatHotend(self): - self._printer._controller.cancelPreheatHotend(self) \ No newline at end of file + def cancelPreheatHotend(self) -> None: + self._printer._controller.cancelPreheatHotend(self) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index b7862251c9..94f86f19a3 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -188,40 +188,55 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if reply in self._kept_alive_multiparts: del self._kept_alive_multiparts[reply] - def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + def _validateManager(self) -> None: if self._manager is None: self._createNetworkManager() assert (self._manager is not None) + + def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + self._validateManager() request = self._createEmptyRequest(target) self._last_request_time = time() - reply = self._manager.put(request, data.encode()) - self._registerOnFinishedCallback(reply, on_finished) + if self._manager is not None: + reply = self._manager.put(request, data.encode()) + self._registerOnFinishedCallback(reply, on_finished) + else: + Logger.log("e", "Could not find manager.") + + def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + self._validateManager() + request = self._createEmptyRequest(target) + self._last_request_time = time() + if self._manager is not None: + reply = self._manager.deleteResource(request) + self._registerOnFinishedCallback(reply, on_finished) + else: + Logger.log("e", "Could not find manager.") def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: - if self._manager is None: - self._createNetworkManager() - assert (self._manager is not None) + self._validateManager() request = self._createEmptyRequest(target) self._last_request_time = time() - reply = self._manager.get(request) - self._registerOnFinishedCallback(reply, on_finished) + if self._manager is not None: + reply = self._manager.get(request) + self._registerOnFinishedCallback(reply, on_finished) + else: + Logger.log("e", "Could not find manager.") def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: - if self._manager is None: - self._createNetworkManager() - assert (self._manager is not None) + self._validateManager() request = self._createEmptyRequest(target) self._last_request_time = time() - reply = self._manager.post(request, data) - if on_progress is not None: - reply.uploadProgress.connect(on_progress) - self._registerOnFinishedCallback(reply, on_finished) + if self._manager is not None: + reply = self._manager.post(request, data) + if on_progress is not None: + reply.uploadProgress.connect(on_progress) + self._registerOnFinishedCallback(reply, on_finished) + else: + Logger.log("e", "Could not find manager.") def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply: - - if self._manager is None: - self._createNetworkManager() - assert (self._manager is not None) + self._validateManager() request = self._createEmptyRequest(target, content_type=None) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for part in parts: @@ -229,15 +244,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_request_time = time() - reply = self._manager.post(request, multi_post_part) + if self._manager is not None: + reply = self._manager.post(request, multi_post_part) - self._kept_alive_multiparts[reply] = multi_post_part + self._kept_alive_multiparts[reply] = multi_post_part - if on_progress is not None: - reply.uploadProgress.connect(on_progress) - self._registerOnFinishedCallback(reply, on_finished) + if on_progress is not None: + reply.uploadProgress.connect(on_progress) + self._registerOnFinishedCallback(reply, on_finished) - return reply + return reply + else: + Logger.log("e", "Could not find manager.") def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: post_part = QHttpPart() diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index b77600f85c..7366b95f86 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -2,11 +2,15 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, List + +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QImage if TYPE_CHECKING: from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + from cura.PrinterOutput.ConfigurationModel import ConfigurationModel class PrintJobOutputModel(QObject): @@ -17,6 +21,9 @@ class PrintJobOutputModel(QObject): keyChanged = pyqtSignal() assignedPrinterChanged = pyqtSignal() ownerChanged = pyqtSignal() + configurationChanged = pyqtSignal() + previewImageChanged = pyqtSignal() + compatibleMachineFamiliesChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: super().__init__(parent) @@ -29,6 +36,48 @@ class PrintJobOutputModel(QObject): self._assigned_printer = None # type: Optional[PrinterOutputModel] self._owner = "" # Who started/owns the print job? + self._configuration = None # type: Optional[ConfigurationModel] + self._compatible_machine_families = [] # type: List[str] + self._preview_image_id = 0 + + self._preview_image = None # type: Optional[QImage] + + @pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged) + def compatibleMachineFamilies(self): + # Hack; Some versions of cluster will return a family more than once... + return set(self._compatible_machine_families) + + def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None: + if self._compatible_machine_families != compatible_machine_families: + self._compatible_machine_families = compatible_machine_families + self.compatibleMachineFamiliesChanged.emit() + + @pyqtProperty(QUrl, notify=previewImageChanged) + def previewImageUrl(self): + self._preview_image_id += 1 + # There is an image provider that is called "camera". In order to ensure that the image qml object, that + # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl + # as new (instead of relying on cached version and thus forces an update. + temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key + return QUrl(temp, QUrl.TolerantMode) + + def getPreviewImage(self) -> Optional[QImage]: + return self._preview_image + + def updatePreviewImage(self, preview_image: Optional[QImage]) -> None: + if self._preview_image != preview_image: + self._preview_image = preview_image + self.previewImageChanged.emit() + + @pyqtProperty(QObject, notify=configurationChanged) + def configuration(self) -> Optional["ConfigurationModel"]: + return self._configuration + + def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None: + if self._configuration != configuration: + self._configuration = configuration + self.configurationChanged.emit() + @pyqtProperty(str, notify=ownerChanged) def owner(self): return self._owner diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index f10d6bd75b..f009a33178 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -35,7 +35,7 @@ class PrinterOutputModel(QObject): self._key = "" # Unique identifier self._controller = output_controller self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] - self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer + self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] self._firmware_version = firmware_version @@ -43,9 +43,9 @@ class PrinterOutputModel(QObject): self._is_preheating = False self._printer_type = "" self._buildplate_name = None - # Update the printer configuration every time any of the extruders changes its configuration - for extruder in self._extruders: - extruder.extruderConfigurationChanged.connect(self._updateExtruderConfiguration) + + self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in + self._extruders] self._camera = None @@ -282,8 +282,4 @@ class PrinterOutputModel(QObject): def printerConfiguration(self): if self._printer_configuration.isValid(): return self._printer_configuration - return None - - def _updateExtruderConfiguration(self): - self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders] - self.configurationChanged.emit() + return None \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/ClusterControlItem.qml b/plugins/UM3NetworkPrinting/ClusterControlItem.qml index 5cf550955c..be72d3c07a 100644 --- a/plugins/UM3NetworkPrinting/ClusterControlItem.qml +++ b/plugins/UM3NetworkPrinting/ClusterControlItem.qml @@ -1,21 +1,26 @@ -import QtQuick 2.2 +import QtQuick 2.3 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.3 +import QtGraphicalEffects 1.0 + +import QtQuick.Controls 2.0 as Controls2 import UM 1.3 as UM import Cura 1.0 as Cura + Component { Rectangle { id: base - property var manager: Cura.MachineManager.printerOutputDevices[0] property var lineColor: "#DCDCDC" // TODO: Should be linked to theme. - property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme. - visible: manager != null + + property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme. + visible: OutputDevice != null anchors.fill: parent - color: UM.Theme.getColor("viewport_background") + color: "white" UM.I18nCatalog { @@ -25,217 +30,689 @@ Component Label { - id: activePrintersLabel + id: printingLabel font: UM.Theme.getFont("large") - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: UM.Theme.getSize("default_margin").height - anchors.top: parent.top - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right:parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width - text: Cura.MachineManager.printerOutputDevices[0].name + anchors + { + margins: 2 * UM.Theme.getSize("default_margin").width + leftMargin: 4 * UM.Theme.getSize("default_margin").width + top: parent.top + left: parent.left + right: parent.right + } + + text: catalog.i18nc("@label", "Printing") elide: Text.ElideRight } - Rectangle + Label { - id: printJobArea - border.width: UM.Theme.getSize("default_lining").width - border.color: lineColor - anchors.top: activePrintersLabel.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - anchors.rightMargin:UM.Theme.getSize("default_margin").width - radius: cornerRadius - height: childrenRect.height - - Item - { - id: printJobTitleBar - width: parent.width - height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height - - Label - { - id: printJobTitleLabel - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.top: parent.top - anchors.topMargin: UM.Theme.getSize("default_margin").height - text: catalog.i18nc("@title", "Print jobs") - font: UM.Theme.getFont("default") - opacity: 0.75 - } - Rectangle - { - anchors.bottom: parent.bottom - height: UM.Theme.getSize("default_lining").width - color: lineColor - width: parent.width - } - } - - Column - { - id: printJobColumn - anchors.top: printJobTitleBar.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width - - Item - { - width: parent.width - height: childrenRect.height - opacity: 0.65 - Label - { - text: catalog.i18nc("@label", "Printing") - font: UM.Theme.getFont("very_small") - - } - Label - { - text: manager.activePrintJobs.length - font: UM.Theme.getFont("small") - anchors.right: parent.right - } - } - Item - { - width: parent.width - height: childrenRect.height - opacity: 0.65 - Label - { - text: catalog.i18nc("@label", "Queued") - font: UM.Theme.getFont("very_small") - } - Label - { - text: manager.queuedPrintJobs.length - font: UM.Theme.getFont("small") - anchors.right: parent.right - } - } - } - OpenPanelButton - { - anchors.top: printJobColumn.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: UM.Theme.getSize("default_margin").height - id: configButton - onClicked: base.manager.openPrintJobControlPanel() - text: catalog.i18nc("@action:button", "View print jobs") - } - - Item - { - // spacer - anchors.top: configButton.bottom - width: UM.Theme.getSize("default_margin").width - height: UM.Theme.getSize("default_margin").height - } + id: managePrintersLabel + anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width + anchors.right: printerScrollView.right + anchors.bottom: printingLabel.bottom + text: catalog.i18nc("@label link to connect manager", "Manage printers") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("primary") + linkColor: UM.Theme.getColor("primary") } - - Rectangle + MouseArea { - id: printersArea - border.width: UM.Theme.getSize("default_lining").width - border.color: lineColor - anchors.top: printJobArea.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - anchors.rightMargin:UM.Theme.getSize("default_margin").width - radius: cornerRadius - height: childrenRect.height + anchors.fill: managePrintersLabel + hoverEnabled: true + onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel() + onEntered: managePrintersLabel.font.underline = true + onExited: managePrintersLabel.font.underline = false + } - Item + ScrollView + { + id: printerScrollView + anchors { - id: printersTitleBar - width: parent.width - height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height - - Label - { - id: printersTitleLabel - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.top: parent.top - anchors.topMargin: UM.Theme.getSize("default_margin").height - text: catalog.i18nc("@label:title", "Printers") - font: UM.Theme.getFont("default") - opacity: 0.75 - } - Rectangle - { - anchors.bottom: parent.bottom - height: UM.Theme.getSize("default_lining").width - color: lineColor - width: parent.width - } + top: printingLabel.bottom + left: parent.left + right: parent.right + topMargin: UM.Theme.getSize("default_margin").height + bottom: parent.bottom + bottomMargin: UM.Theme.getSize("default_margin").height } - Column - { - id: printersColumn - anchors.top: printersTitleBar.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width - Repeater + style: UM.Theme.styles.scrollview + + ListView + { + anchors { - model: manager.connectedPrintersTypeCount - Item + top: parent.top + bottom: parent.bottom + left: parent.left + right: parent.right + leftMargin: 2 * UM.Theme.getSize("default_margin").width + rightMargin: 2 * UM.Theme.getSize("default_margin").width + } + spacing: UM.Theme.getSize("default_margin").height -10 + model: OutputDevice.printers + + delegate: Item + { + width: parent.width + height: base.height + 2 * base.shadowRadius // To ensure that the shadow doesn't get cut off. + Rectangle { - width: parent.width - height: childrenRect.height - opacity: 0.65 - Label + width: parent.width - 2 * shadowRadius + height: childrenRect.height + UM.Theme.getSize("default_margin").height + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + id: base + property var shadowRadius: 5 + property var collapsed: true + + layer.enabled: true + layer.effect: DropShadow { - text: modelData.machine_type - font: UM.Theme.getFont("very_small") + radius: base.shadowRadius + verticalOffset: 2 + color: "#3F000000" // 25% shadow } - Label + Item { - text: modelData.count - font: UM.Theme.getFont("small") - anchors.right: parent.right + id: printerInfo + height: machineIcon.height + anchors + { + top: parent.top + left: parent.left + right: parent.right + margins: UM.Theme.getSize("default_margin").width + } + + MouseArea + { + anchors.fill: parent + onClicked: base.collapsed = !base.collapsed + } + + Item + { + id: machineIcon + // Yeah, this is hardcoded now, but I can't think of a good way to fix this. + // The UI is going to get another update soon, so it's probably not worth the effort... + width: 58 + height: 58 + anchors.top: parent.top + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.left: parent.left + + UM.RecolorImage + { + anchors.centerIn: parent + source: + { + switch(modelData.type) + { + case "Ultimaker 3": + return "UM3-icon.svg" + case "Ultimaker 3 Extended": + return "UM3x-icon.svg" + case "Ultimaker S5": + return "UMs5-icon.svg" + } + } + width: sourceSize.width + height: sourceSize.height + + color: + { + if(modelData.state == "disabled") + { + return UM.Theme.getColor("setting_control_disabled") + } + + if(modelData.activePrintJob != undefined) + { + return UM.Theme.getColor("primary") + } + + return UM.Theme.getColor("setting_control_disabled") + } + } + } + Item + { + height: childrenRect.height + anchors + { + right: collapseIcon.left + rightMargin: UM.Theme.getSize("default_margin").width + left: machineIcon.right + leftMargin: UM.Theme.getSize("default_margin").width + + verticalCenter: machineIcon.verticalCenter + } + + Label + { + id: machineNameLabel + text: modelData.name + width: parent.width + elide: Text.ElideRight + font: UM.Theme.getFont("default_bold") + } + + Label + { + id: activeJobLabel + text: + { + if (modelData.state == "disabled") + { + return catalog.i18nc("@label", "Not available") + } else if (modelData.state == "unreachable") + { + return catalog.i18nc("@label", "Unreachable") + } + if (modelData.activePrintJob != null) + { + return modelData.activePrintJob.name + } + return catalog.i18nc("@label", "Available") + } + anchors.top: machineNameLabel.bottom + width: parent.width + elide: Text.ElideRight + font: UM.Theme.getFont("default") + opacity: 0.6 + } + } + + UM.RecolorImage + { + id: collapseIcon + width: 15 + height: 15 + sourceSize.width: width + sourceSize.height: height + source: base.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom") + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + color: "black" + } + } + + Item + { + id: detailedInfo + property var printJob: modelData.activePrintJob + visible: height == childrenRect.height + anchors.top: printerInfo.bottom + width: parent.width + height: !base.collapsed ? childrenRect.height : 0 + opacity: visible ? 1 : 0 + Behavior on height { NumberAnimation { duration: 100 } } + Behavior on opacity { NumberAnimation { duration: 100 } } + Rectangle + { + id: topSpacer + color: UM.Theme.getColor("viewport_background") + height: 2 + anchors + { + left: parent.left + right: parent.right + margins: UM.Theme.getSize("default_margin").width + top: parent.top + topMargin: UM.Theme.getSize("default_margin").width + } + } + PrinterFamilyPill + { + id: printerFamilyPill + color: UM.Theme.getColor("viewport_background") + anchors.top: topSpacer.bottom + anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height + text: modelData.type + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + padding: 3 + } + Row + { + id: extrudersInfo + anchors.top: printerFamilyPill.bottom + anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height + anchors.left: parent.left + anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width + anchors.right: parent.right + anchors.rightMargin: 2 * UM.Theme.getSize("default_margin").width + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + + PrintCoreConfiguration + { + id: leftExtruderInfo + width: Math.round(parent.width / 2) + printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[0] + } + + PrintCoreConfiguration + { + id: rightExtruderInfo + width: Math.round(parent.width / 2) + printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[1] + } + } + + Rectangle + { + id: jobSpacer + color: UM.Theme.getColor("viewport_background") + height: 2 + anchors + { + left: parent.left + right: parent.right + margins: UM.Theme.getSize("default_margin").width + top: extrudersInfo.bottom + topMargin: 2 * UM.Theme.getSize("default_margin").height + } + } + + Item + { + id: jobInfo + property var showJobInfo: modelData.activePrintJob != null && modelData.activePrintJob.state != "queued" + + anchors.top: jobSpacer.bottom + anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: UM.Theme.getSize("default_margin").width + anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width + height: showJobInfo ? childrenRect.height + 2 * UM.Theme.getSize("default_margin").height: 0 + visible: showJobInfo + Label + { + id: printJobName + text: modelData.activePrintJob != null ? modelData.activePrintJob.name : "" + font: UM.Theme.getFont("default_bold") + anchors.left: parent.left + anchors.right: contextButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width + elide: Text.ElideRight + } + Label + { + id: ownerName + anchors.top: printJobName.bottom + text: modelData.activePrintJob != null ? modelData.activePrintJob.owner : "" + font: UM.Theme.getFont("default") + opacity: 0.6 + width: parent.width + elide: Text.ElideRight + } + + function switchPopupState() + { + if (popup.visible) + { + popup.close() + } + else + { + popup.open() + } + } + + Controls2.Button + { + id: contextButton + text: "\u22EE" //Unicode; Three stacked points. + font.pixelSize: 25 + width: 35 + height: width + anchors + { + right: parent.right + top: parent.top + } + hoverEnabled: true + + background: Rectangle + { + opacity: contextButton.down || contextButton.hovered ? 1 : 0 + width: contextButton.width + height: contextButton.height + radius: 0.5 * width + color: UM.Theme.getColor("viewport_background") + } + + onClicked: parent.switchPopupState() + } + + Controls2.Popup + { + // TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property + id: popup + clip: true + closePolicy: Controls2.Popup.CloseOnPressOutsideParent + x: parent.width - width + y: contextButton.height + width: 160 + height: contentItem.height + 2 * padding + visible: false + + transformOrigin: Controls2.Popup.Top + contentItem: Item + { + width: popup.width - 2 * popup.padding + height: childrenRect.height + 15 + Controls2.Button + { + id: pauseButton + text: modelData.activePrintJob != null && modelData.activePrintJob.state == "paused" ? catalog.i18nc("@label", "Resume") : catalog.i18nc("@label", "Pause") + onClicked: + { + if(modelData.activePrintJob.state == "paused") + { + modelData.activePrintJob.setState("print") + } + else if(modelData.activePrintJob.state == "printing") + { + modelData.activePrintJob.setState("pause") + } + popup.close() + } + width: parent.width + enabled: modelData.activePrintJob != null && ["paused", "printing"].indexOf(modelData.activePrintJob.state) >= 0 + anchors.top: parent.top + anchors.topMargin: 10 + hoverEnabled: true + background: Rectangle + { + opacity: pauseButton.down || pauseButton.hovered ? 1 : 0 + color: UM.Theme.getColor("viewport_background") + } + } + + Controls2.Button + { + id: abortButton + text: catalog.i18nc("@label", "Abort") + onClicked: + { + modelData.activePrintJob.setState("abort") + popup.close() + } + width: parent.width + anchors.top: pauseButton.bottom + hoverEnabled: true + enabled: modelData.activePrintJob != null && ["paused", "printing", "pre_print"].indexOf(modelData.activePrintJob.state) >= 0 + background: Rectangle + { + opacity: abortButton.down || abortButton.hovered ? 1 : 0 + color: UM.Theme.getColor("viewport_background") + } + } + } + + background: Item + { + width: popup.width + height: popup.height + + DropShadow + { + anchors.fill: pointedRectangle + radius: 5 + color: "#3F000000" // 25% shadow + source: pointedRectangle + transparentBorder: true + verticalOffset: 2 + } + + Item + { + id: pointedRectangle + width: parent.width -10 + height: parent.height -10 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + Rectangle + { + id: point + height: 13 + width: 13 + color: UM.Theme.getColor("setting_control") + transform: Rotation { angle: 45} + anchors.right: bloop.right + y: 1 + } + + Rectangle + { + id: bloop + color: UM.Theme.getColor("setting_control") + width: parent.width + anchors.top: parent.top + anchors.topMargin: 10 + anchors.bottom: parent.bottom + anchors.bottomMargin: 5 + } + } + } + + exit: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + NumberAnimation { property: "visible"; duration: 75; } + } + enter: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + NumberAnimation { property: "visible"; duration: 75; } + } + + onClosed: visible = false + onOpened: visible = true + } + + Image + { + id: printJobPreview + source: modelData.activePrintJob != null ? modelData.activePrintJob.previewImageUrl : "" + anchors.top: ownerName.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width / 2 + height: width + opacity: + { + if(modelData.activePrintJob == null) + { + return 1.0 + } + + switch(modelData.activePrintJob.state) + { + case "wait_cleanup": + case "wait_user_action": + case "paused": + return 0.5 + default: + return 1.0 + } + } + + + } + + UM.RecolorImage + { + id: statusImage + anchors.centerIn: printJobPreview + source: + { + if(modelData.activePrintJob == null) + { + return "" + } + switch(modelData.activePrintJob.state) + { + case "paused": + return "paused-icon.svg" + case "wait_cleanup": + if(modelData.activePrintJob.timeElapsed < modelData.activePrintJob.timeTotal) + { + return "aborted-icon.svg" + } + return "approved-icon.svg" + case "wait_user_action": + return "aborted-icon.svg" + default: + return "" + } + } + visible: source != "" + width: 0.5 * printJobPreview.width + height: 0.5 * printJobPreview.height + sourceSize.width: width + sourceSize.height: height + color: "black" + } + + Rectangle + { + id: showCameraIcon + width: 35 * screenScaleFactor + height: width + radius: 0.5 * width + anchors.left: parent.left + anchors.bottom: printJobPreview.bottom + color: UM.Theme.getColor("setting_control_border_highlight") + Image + { + width: parent.width + height: width + anchors.right: parent.right + anchors.rightMargin: parent.rightMargin + source: "camera-icon.svg" + } + MouseArea + { + anchors.fill:parent + onClicked: + { + OutputDevice.setActiveCamera(modelData.camera) + } + } + } + } + } + + ProgressBar + { + property var progress: + { + if(modelData.activePrintJob == null) + { + return 0 + } + var result = modelData.activePrintJob.timeElapsed / modelData.activePrintJob.timeTotal + if(result > 1.0) + { + result = 1.0 + } + return result + } + + id: jobProgressBar + width: parent.width + value: progress + anchors.top: detailedInfo.bottom + anchors.topMargin: UM.Theme.getSize("default_margin").height + + visible: modelData.activePrintJob != null && modelData.activePrintJob != undefined + + style: ProgressBarStyle + { + property var progressText: + { + if(modelData.activePrintJob == null) + { + return "" + } + + switch(modelData.activePrintJob.state) + { + case "wait_cleanup": + if(modelData.activePrintJob.timeTotal > modelData.activePrintJob.timeElapsed) + { + return catalog.i18nc("@label:status", "Aborted") + } + return catalog.i18nc("@label:status", "Finished") + case "pre_print": + case "sent_to_printer": + return catalog.i18nc("@label:status", "Preparing") + case "aborted": + case "wait_user_action": + return catalog.i18nc("@label:status", "Aborted") + case "pausing": + return catalog.i18nc("@label:status", "Pausing") + case "paused": + return catalog.i18nc("@label:status", "Paused") + case "resuming": + return catalog.i18nc("@label:status", "Resuming") + case "queued": + return catalog.i18nc("@label:status", "Action required") + default: + OutputDevice.formatDuration(modelData.activePrintJob.timeTotal - modelData.activePrintJob.timeElapsed) + } + } + + background: Rectangle + { + implicitWidth: 100 + implicitHeight: visible ? 24 : 0 + color: UM.Theme.getColor("viewport_background") + } + + progress: Rectangle + { + color: UM.Theme.getColor("primary") + id: progressItem + function getTextOffset() + { + if(progressItem.width + progressLabel.width < control.width) + { + return progressItem.width + UM.Theme.getSize("default_margin").width + } + else + { + return progressItem.width - progressLabel.width - UM.Theme.getSize("default_margin").width + } + } + + Label + { + id: progressLabel + anchors.left: parent.left + anchors.leftMargin: getTextOffset() + text: progressText + anchors.verticalCenter: parent.verticalCenter + color: progressItem.width + progressLabel.width < control.width ? "black" : "white" + width: contentWidth + font: UM.Theme.getFont("default") + } + } + } } } } } - OpenPanelButton - { - anchors.top: printersColumn.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: UM.Theme.getSize("default_margin").height - id: printerConfigButton - onClicked: base.manager.openPrinterControlPanel() - - text: catalog.i18nc("@action:button", "View printers") - } - - Item - { - // spacer - anchors.top: printerConfigButton.bottom - width: UM.Theme.getSize("default_margin").width - height: UM.Theme.getSize("default_margin").height - } } } } diff --git a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml index 0e86d55de8..71b598d05c 100644 --- a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml +++ b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml @@ -25,93 +25,83 @@ Component Label { - id: activePrintersLabel - font: UM.Theme.getFont("large") - - anchors { - top: parent.top - topMargin: UM.Theme.getSize("default_margin").height * 2 // a bit more spacing to give it some breathing room - horizontalCenter: parent.horizontalCenter - } - - text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : "" - - visible: OutputDevice.printers.length == 0 + id: manageQueueLabel + anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width + anchors.right: queuedPrintJobs.right + anchors.bottom: queuedLabel.bottom + text: catalog.i18nc("@label link to connect manager", "Manage queue") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("primary") + linkColor: UM.Theme.getColor("primary") } - Item + MouseArea { - anchors.topMargin: UM.Theme.getSize("default_margin").height + anchors.fill: manageQueueLabel + hoverEnabled: true + onClicked: Cura.MachineManager.printerOutputDevices[0].openPrintJobControlPanel() + onEntered: manageQueueLabel.font.underline = true + onExited: manageQueueLabel.font.underline = false + } + + Label + { + id: queuedLabel + anchors.left: queuedPrintJobs.left anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - - width: Math.min(800 * screenScaleFactor, maximumWidth) - height: children.height - visible: OutputDevice.printers.length != 0 - - Label - { - id: addRemovePrintersLabel - anchors.right: parent.right - text: catalog.i18nc("@label link to connect manager", "Add/Remove printers") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - linkColor: UM.Theme.getColor("text_link") - } - - MouseArea - { - anchors.fill: addRemovePrintersLabel - hoverEnabled: true - onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel() - onEntered: addRemovePrintersLabel.font.underline = true - onExited: addRemovePrintersLabel.font.underline = false - } + anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height + anchors.leftMargin: 3 * UM.Theme.getSize("default_margin").width + text: catalog.i18nc("@label", "Queued") + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") } ScrollView { - id: printerScrollView - anchors.margins: UM.Theme.getSize("default_margin").width - anchors.top: activePrintersLabel.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_lining").width // To ensure border can be drawn. - anchors.rightMargin: UM.Theme.getSize("default_lining").width - anchors.right: parent.right + id: queuedPrintJobs + anchors + { + top: queuedLabel.bottom + topMargin: UM.Theme.getSize("default_margin").height + horizontalCenter: parent.horizontalCenter + bottomMargin: 0 + bottom: parent.bottom + } + style: UM.Theme.styles.scrollview + width: Math.min(800 * screenScaleFactor, maximumWidth) ListView { anchors.fill: parent - spacing: -UM.Theme.getSize("default_lining").height + //anchors.margins: UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("default_margin").height - 10 // 2x the shadow radius - model: OutputDevice.printers + model: OutputDevice.queuedPrintJobs - delegate: PrinterInfoBlock + delegate: PrintJobInfoBlock { - printer: modelData - width: Math.min(800 * screenScaleFactor, maximumWidth) - height: 125 * screenScaleFactor - - // Add a 1 pix margin, as the border is sometimes cut off otherwise. - anchors.horizontalCenter: parent.horizontalCenter + printJob: modelData + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").height + anchors.leftMargin: UM.Theme.getSize("default_margin").height + height: 175 * screenScaleFactor } } } PrinterVideoStream { - visible: OutputDevice.activePrinter != null - anchors.fill:parent + visible: OutputDevice.activeCamera != null + anchors.fill: parent + camera: OutputDevice.activeCamera } onVisibleChanged: { - if (!monitorFrame.visible) + if (monitorFrame != null && !monitorFrame.visible) { - // After switching the Tab ensure that active printer is Null, the video stream image - // might be active - OutputDevice.setActivePrinter(null) + OutputDevice.setActiveCamera(null) } } } diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index e85961f619..8345de049c 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -4,19 +4,21 @@ from typing import Any, cast, Optional, Set, Tuple, Union from UM.FileHandler.FileHandler import FileHandler -from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary). -from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously. +from UM.FileHandler.FileWriter import FileWriter # To choose based on the output file mode (text vs. binary). +from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously. from UM.Logger import Logger from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog -from UM.Mesh.MeshWriter import MeshWriter # For typing + from UM.Message import Message from UM.Qt.Duration import Duration, DurationFormat -from UM.OutputDevice import OutputDeviceError #To show that something went wrong when writing. -from UM.Scene.SceneNode import SceneNode #For typing. -from UM.Version import Version #To check against firmware versions for support. +from UM.OutputDevice import OutputDeviceError # To show that something went wrong when writing. +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.ConfigurationModel import ConfigurationModel +from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel @@ -27,14 +29,14 @@ from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController from .SendMaterialJob import SendMaterialJob from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply -from PyQt5.QtGui import QDesktopServices +from PyQt5.QtGui import QDesktopServices, QImage from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject from time import time from datetime import datetime -from typing import Optional, Dict, List, Set +from typing import Optional, Dict, List -import io #To create the correct buffers for sending data to the printer. +import io # To create the correct buffers for sending data to the printer. import json import os @@ -44,6 +46,7 @@ i18n_catalog = i18nCatalog("cura") class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() activePrinterChanged = pyqtSignal() + activeCameraChanged = pyqtSignal() # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. @@ -65,18 +68,18 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) - self._accepts_commands = True #type: bool + self._accepts_commands = True # type: bool # Cluster does not have authentication, so default to authenticated self._authentication_state = AuthState.Authenticated - self._error_message = None #type: Optional[Message] - self._write_job_progress_message = None #type: Optional[Message] - self._progress_message = None #type: Optional[Message] + self._error_message = None # type: Optional[Message] + self._write_job_progress_message = None # type: Optional[Message] + self._progress_message = None # type: Optional[Message] self._active_printer = None # type: Optional[PrinterOutputModel] - self._printer_selection_dialog = None #type: QObject + self._printer_selection_dialog = None # type: QObject self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(self._id) @@ -87,32 +90,35 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] - self._finished_jobs = [] # type: List[PrintJobOutputModel] + self._finished_jobs = [] # type: List[PrintJobOutputModel] - self._cluster_size = int(properties.get(b"cluster_size", 0)) + self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int - self._latest_reply_handler = None #type: Optional[QNetworkReply] + self._latest_reply_handler = None # type: Optional[QNetworkReply] + self._sending_job = None + + self._active_camera = None # type: Optional[NetworkCamera] def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: self.writeStarted.emit(self) self.sendMaterialProfiles() - #Formats supported by this application (file types that we can actually write). + # Formats supported by this application (file types that we can actually write). if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() else: file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() global_stack = CuraApplication.getInstance().getGlobalContainerStack() - #Create a list from the supported file formats string. + # Create a list from the supported file formats string. if not global_stack: Logger.log("e", "Missing global stack!") return machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";") machine_file_formats = [file_type.strip() for file_type in machine_file_formats] - #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. + # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"): machine_file_formats = ["application/x-ufp"] + machine_file_formats @@ -125,7 +131,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!")) preferred_format = file_formats[0] - #Just take the first file format available. + # Just take the first file format available. if file_handler is not None: writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"])) else: @@ -135,19 +141,20 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): Logger.log("e", "Unexpected error when trying to get the FileWriter") return - #This function pauses with the yield, waiting on instructions on which printer it needs to print with. + # This function pauses with the yield, waiting on instructions on which printer it needs to print with. if not writer: Logger.log("e", "Missing file or mesh writer!") return self._sending_job = self._sendPrintJob(writer, preferred_format, nodes) - self._sending_job.send(None) #Start the generator. + if self._sending_job is not None: + self._sending_job.send(None) # Start the generator. - if len(self._printers) > 1: #We need to ask the user. - self._spawnPrinterSelectionDialog() - is_job_sent = True - else: #Just immediately continue. - self._sending_job.send("") #No specifically selected printer. - is_job_sent = self._sending_job.send(None) + if len(self._printers) > 1: # We need to ask the user. + self._spawnPrinterSelectionDialog() + is_job_sent = True + else: # Just immediately continue. + self._sending_job.send("") # No specifically selected printer. + is_job_sent = self._sending_job.send(None) def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: @@ -157,7 +164,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._printer_selection_dialog.show() @pyqtProperty(int, constant=True) - def clusterSize(self): + def clusterSize(self) -> int: return self._cluster_size ## Allows the user to choose a printer to print with from the printer @@ -165,7 +172,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # \param target_printer The name of the printer to target. @pyqtSlot(str) def selectPrinter(self, target_printer: str = "") -> None: - self._sending_job.send(target_printer) + if self._sending_job is not None: + self._sending_job.send(target_printer) @pyqtSlot() def cancelPrintSelection(self) -> None: @@ -214,8 +222,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): job.start() - yield True #Return that we had success! - yield #To prevent having to catch the StopIteration exception. + yield True # Return that we had success! + yield # To prevent having to catch the StopIteration exception. def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: if self._write_job_progress_message: @@ -240,7 +248,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] - output = stream.getvalue() #Either str or bytes depending on the output mode. + output = stream.getvalue() # Either str or bytes depending on the output mode. if isinstance(stream, io.StringIO): output = cast(str, output).encode("utf-8") output = cast(bytes, output) @@ -253,6 +261,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def activePrinter(self) -> Optional[PrinterOutputModel]: return self._active_printer + @pyqtProperty(QObject, notify=activeCameraChanged) + def activeCamera(self) -> Optional[NetworkCamera]: + return self._active_camera + @pyqtSlot(QObject) def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: if self._active_printer != printer: @@ -261,6 +273,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._active_printer = printer self.activePrinterChanged.emit() + @pyqtSlot(QObject) + def setActiveCamera(self, camera: Optional[NetworkCamera]) -> None: + if self._active_camera != camera: + if self._active_camera: + self._active_camera.stop() + + self._active_camera = camera + + if self._active_camera: + self._active_camera.start() + + self.activeCameraChanged.emit() + def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: if self._progress_message: self._progress_message.hide() @@ -279,8 +304,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # If successfully sent: if bytes_sent == bytes_total: - # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the - # monitor tab. + # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to + # the monitor tab. self._success_message = Message( i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), lifetime=5, dismissable=True, @@ -329,7 +354,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): @pyqtProperty("QVariantList", notify = printJobsChanged) def queuedPrintJobs(self) -> List[PrintJobOutputModel]: - return [print_job for print_job in self._print_jobs if print_job.state == "queued"] + return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"] @pyqtProperty("QVariantList", notify = printJobsChanged) def activePrintJobs(self) -> List[PrintJobOutputModel]: @@ -348,6 +373,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])}) return result + @pyqtProperty("QVariantList", notify=clusterPrintersChanged) + def printers(self): + return self._printers + @pyqtSlot(int, result = str) def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) @@ -364,6 +393,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() + @pyqtSlot(str) + def sendJobToTop(self, print_job_uuid: str) -> None: + # This function is part of the output device (and not of the printjob output model) as this type of operation + # is a modification of the cluster queue and not of the actual job. + data = "{\"to_position\": 0}" + self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None) + + @pyqtSlot(str) + def deleteJobFromQueue(self, print_job_uuid: str) -> None: + # This function is part of the output device (and not of the printjob output model) as this type of operation + # 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) + def _printJobStateChanged(self) -> None: username = self._getUserName() @@ -392,11 +434,26 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): super().connect() self.sendMaterialProfiles() + def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: + reply_url = reply.url().toString() + + uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")] + + print_job = findByKey(self._print_jobs, uuid) + if print_job: + image = QImage() + image.loadFromData(reply.readAll()) + print_job.updatePreviewImage(image) + def _update(self) -> None: super()._update() self.get("printers/", on_finished = self._onGetPrintersDataFinished) self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished) + for print_job in self._print_jobs: + if print_job.getPreviewImage() is None: + self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) + def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return @@ -407,16 +464,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_jobs_seen = [] job_list_changed = False - for print_job_data in result: + for idx, print_job_data in enumerate(result): print_job = findByKey(self._print_jobs, print_job_data["uuid"]) - if print_job is None: print_job = self._createPrintJobModel(print_job_data) job_list_changed = True + elif not job_list_changed: + # Check if the order of the jobs has changed since the last check + if self._print_jobs.index(print_job) != idx: + job_list_changed = True self._updatePrintJob(print_job, print_job_data) - if print_job.state != "queued": # Print job should be assigned to a printer. + if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer. if print_job.state in ["failed", "finished", "aborted", "none"]: # Print job was already completed, so don't attach it to a printer. printer = None @@ -437,6 +497,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): job_list_changed = job_list_changed or self._removeJob(removed_job) if job_list_changed: + # Override the old list with the new list (either because jobs were removed / added or order changed) + self._print_jobs = print_jobs_seen self.printJobsChanged.emit() # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: @@ -478,16 +540,59 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def _createPrintJobModel(self, data: Dict[str, Any]) -> PrintJobOutputModel: print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name= data["name"]) + + configuration = ConfigurationModel() + extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)] + for index in range(0, self._number_of_extruders): + try: + extruder_data = data["configuration"][index] + except IndexError: + continue + extruder = extruders[int(data["configuration"][index]["extruder_index"])] + extruder.setHotendID(extruder_data.get("print_core_id", "")) + extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {}))) + + configuration.setExtruderConfigurations(extruders) + print_job.updateConfiguration(configuration) + print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", [])) print_job.stateChanged.connect(self._printJobStateChanged) - self._print_jobs.append(print_job) return print_job def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict[str, Any]) -> None: print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) - print_job.updateState(data["status"]) + impediments_to_printing = data.get("impediments_to_printing", []) print_job.updateOwner(data["owner"]) + status_set_by_impediment = False + for impediment in impediments_to_printing: + if impediment["severity"] == "UNFIXABLE": + status_set_by_impediment = True + print_job.updateState("error") + break + + if not status_set_by_impediment: + print_job.updateState(data["status"]) + + + def _createMaterialOutputModel(self, material_data) -> MaterialOutputModel: + containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_data["guid"]) + if containers: + color = containers[0].getMetaDataEntry("color_code") + brand = containers[0].getMetaDataEntry("brand") + material_type = containers[0].getMetaDataEntry("material") + name = containers[0].getName() + else: + Logger.log("w", + "Unable to find material with guid {guid}. Using data as provided by cluster".format( + guid=material_data["guid"])) + color = material_data["color"] + brand = material_data["brand"] + material_type = material_data["material"] + name = "Empty" if material_data["material"] == "empty" else "Unknown" + return MaterialOutputModel(guid=material_data["guid"], type=material_type, + brand=brand, color=color, name=name) + def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None: # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. @@ -523,24 +628,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): material_data = extruder_data["material"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: - containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", - GUID=material_data["guid"]) - if containers: - color = containers[0].getMetaDataEntry("color_code") - brand = containers[0].getMetaDataEntry("brand") - material_type = containers[0].getMetaDataEntry("material") - name = containers[0].getName() - else: - Logger.log("w", - "Unable to find material with guid {guid}. Using data as provided by cluster".format( - guid=material_data["guid"])) - color = material_data["color"] - brand = material_data["brand"] - material_type = material_data["material"] - name = "Empty" if material_data["material"] == "empty" else "Unknown" - - material = MaterialOutputModel(guid=material_data["guid"], type=material_type, - brand=brand, color=color, name=name) + material = self._createMaterialOutputModel(material_data) extruder.updateActiveMaterial(material) def _removeJob(self, job: PrintJobOutputModel) -> bool: @@ -568,6 +656,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): job = SendMaterialJob(device = self) job.run() + def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) @@ -586,8 +675,8 @@ def checkValidGetReply(reply: QNetworkReply) -> bool: return True -def findByKey(list: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]: - for item in list: +def findByKey(lst: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]: + for item in lst: if item.key == key: return item return None diff --git a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py index 4a0319cafc..fcced0b883 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py @@ -6,8 +6,6 @@ from cura.PrinterOutput.PrinterOutputController import PrinterOutputController MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel - from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel - class ClusterUM3PrinterOutputController(PrinterOutputController): def __init__(self, output_device): diff --git a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml index 267516091b..0ae1fec920 100644 --- a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml +++ b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml @@ -12,22 +12,82 @@ Item width: Math.round(parent.width / 2) height: childrenRect.height - Label + + Item { - id: materialLabel - text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : "" - elide: Text.ElideRight - width: parent.width - font: UM.Theme.getFont("very_small") + id: extruderCircle + width: 30 + height: 30 + + anchors.verticalCenter: printAndMaterialLabel.verticalCenter + opacity: + { + if(printCoreConfiguration == null || printCoreConfiguration.activeMaterial == null || printCoreConfiguration.hotendID == null) + { + return 0.5 + } + return 1 + } + + Rectangle + { + anchors.fill: parent + radius: Math.round(width / 2) + border.width: 2 + border.color: "black" + } + + Label + { + anchors.centerIn: parent + font: UM.Theme.getFont("default_bold") + text: printCoreConfiguration.position + 1 + } } - Label + + Item { - id: printCoreLabel - text: printCoreConfiguration.hotendID - anchors.top: materialLabel.bottom - elide: Text.ElideRight - width: parent.width - font: UM.Theme.getFont("very_small") - opacity: 0.5 + id: printAndMaterialLabel + anchors + { + right: parent.right + left: extruderCircle.right + margins: UM.Theme.getSize("default_margin").width + } + height: childrenRect.height + + Label + { + id: materialLabel + text: + { + if(printCoreConfiguration != undefined && printCoreConfiguration.activeMaterial != undefined) + { + return printCoreConfiguration.activeMaterial.name + } + return "" + } + font: UM.Theme.getFont("default_bold") + elide: Text.ElideRight + width: parent.width + } + + Label + { + id: printCoreLabel + text: + { + if(printCoreConfiguration != undefined && printCoreConfiguration.hotendID != undefined) + { + return printCoreConfiguration.hotendID + } + return "" + } + anchors.top: materialLabel.bottom + elide: Text.ElideRight + width: parent.width + opacity: 0.6 + font: UM.Theme.getFont("default") + } } } diff --git a/plugins/UM3NetworkPrinting/PrintJobInfoBlock.qml b/plugins/UM3NetworkPrinting/PrintJobInfoBlock.qml new file mode 100644 index 0000000000..d50ee769d3 --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrintJobInfoBlock.qml @@ -0,0 +1,378 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.0 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +import UM 1.3 as UM + + +Item +{ + id: base + property var printJob: null + property var shadowRadius: 5 + function getPrettyTime(time) + { + return OutputDevice.formatDuration(time) + } + + UM.I18nCatalog + { + id: catalog + name: "cura" + } + + Rectangle + { + id: background + anchors + { + top: parent.top + topMargin: 3 + left: parent.left + leftMargin: base.shadowRadius + rightMargin: base.shadowRadius + right: parent.right + bottom: parent.bottom + bottomMargin: base.shadowRadius + } + + layer.enabled: true + layer.effect: DropShadow + { + radius: base.shadowRadius + verticalOffset: 2 + color: "#3F000000" // 25% shadow + } + + Item + { + // Content on the left of the infobox + anchors + { + top: parent.top + bottom: parent.bottom + left: parent.left + right: parent.horizontalCenter + margins: 2 * UM.Theme.getSize("default_margin").width + rightMargin: UM.Theme.getSize("default_margin").width + } + + Label + { + id: printJobName + text: printJob.name + font: UM.Theme.getFont("default_bold") + width: parent.width + elide: Text.ElideRight + } + + Label + { + id: ownerName + anchors.top: printJobName.bottom + text: printJob.owner + font: UM.Theme.getFont("default") + opacity: 0.6 + width: parent.width + elide: Text.ElideRight + } + + Image + { + id: printJobPreview + source: printJob.previewImageUrl + anchors.top: ownerName.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: totalTimeLabel.bottom + width: height + opacity: printJob.state == "error" ? 0.5 : 1.0 + } + + UM.RecolorImage + { + id: statusImage + anchors.centerIn: printJobPreview + source: printJob.state == "error" ? "aborted-icon.svg" : "" + visible: source != "" + width: 0.5 * printJobPreview.width + height: 0.5 * printJobPreview.height + sourceSize.width: width + sourceSize.height: height + color: "black" + } + + Label + { + id: totalTimeLabel + opacity: 0.6 + anchors.bottom: parent.bottom + anchors.right: parent.right + font: UM.Theme.getFont("default") + text: printJob != null ? getPrettyTime(printJob.timeTotal) : "" + elide: Text.ElideRight + } + } + + Item + { + // Content on the right side of the infobox. + anchors + { + top: parent.top + bottom: parent.bottom + left: parent.horizontalCenter + right: parent.right + margins: 2 * UM.Theme.getSize("default_margin").width + leftMargin: UM.Theme.getSize("default_margin").width + } + + Label + { + id: targetPrinterLabel + elide: Text.ElideRight + font: UM.Theme.getFont("default_bold") + text: + { + if(printJob.assignedPrinter == null) + { + if(printJob.state == "error") + { + return catalog.i18nc("@label", "Waiting for: Unavailable printer") + } + return catalog.i18nc("@label", "Waiting for: First available") + } + else + { + return catalog.i18nc("@label", "Waiting for: ") + printJob.assignedPrinter.name + } + + } + + anchors + { + left: parent.left + right: contextButton.left + rightMargin: UM.Theme.getSize("default_margin").width + } + } + + + function switchPopupState() + { + popup.visible ? popup.close() : popup.open() + } + + Button + { + id: contextButton + text: "\u22EE" //Unicode; Three stacked points. + font.pixelSize: 25 + width: 35 + height: width + anchors + { + right: parent.right + top: parent.top + } + hoverEnabled: true + + background: Rectangle + { + opacity: contextButton.down || contextButton.hovered ? 1 : 0 + width: contextButton.width + height: contextButton.height + radius: 0.5 * width + color: UM.Theme.getColor("viewport_background") + } + + onClicked: parent.switchPopupState() + } + + Popup + { + // TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property + id: popup + clip: true + closePolicy: Popup.CloseOnPressOutsideParent + x: parent.width - width + y: contextButton.height + width: 160 + height: contentItem.height + 2 * padding + visible: false + + transformOrigin: Popup.Top + contentItem: Item + { + width: popup.width - 2 * popup.padding + height: childrenRect.height + 15 + Button + { + id: sendToTopButton + text: catalog.i18nc("@label", "Move to top") + onClicked: + { + OutputDevice.sendJobToTop(printJob.key) + popup.close() + } + width: parent.width + enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key + anchors.top: parent.top + anchors.topMargin: 10 + hoverEnabled: true + background: Rectangle + { + opacity: sendToTopButton.down || sendToTopButton.hovered ? 1 : 0 + color: UM.Theme.getColor("viewport_background") + } + } + + Button + { + id: deleteButton + text: catalog.i18nc("@label", "Delete") + onClicked: + { + OutputDevice.deleteJobFromQueue(printJob.key) + popup.close() + } + width: parent.width + anchors.top: sendToTopButton.bottom + hoverEnabled: true + background: Rectangle + { + opacity: deleteButton.down || deleteButton.hovered ? 1 : 0 + color: UM.Theme.getColor("viewport_background") + } + } + } + + background: Item + { + width: popup.width + height: popup.height + + DropShadow + { + anchors.fill: pointedRectangle + radius: 5 + color: "#3F000000" // 25% shadow + source: pointedRectangle + transparentBorder: true + verticalOffset: 2 + } + + Item + { + id: pointedRectangle + width: parent.width -10 + height: parent.height -10 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + Rectangle + { + id: point + height: 13 + width: 13 + color: UM.Theme.getColor("setting_control") + transform: Rotation { angle: 45} + anchors.right: bloop.right + y: 1 + } + + Rectangle + { + id: bloop + color: UM.Theme.getColor("setting_control") + width: parent.width + anchors.top: parent.top + anchors.topMargin: 10 + anchors.bottom: parent.bottom + anchors.bottomMargin: 5 + } + } + } + + exit: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + NumberAnimation { property: "visible"; duration: 75; } + } + enter: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + NumberAnimation { property: "visible"; duration: 75; } + } + + onClosed: visible = false + onOpened: visible = true + } + + Row + { + id: printerFamilyPills + spacing: 0.5 * UM.Theme.getSize("default_margin").width + anchors + { + left: parent.left + right: parent.right + bottom: extrudersInfo.top + bottomMargin: UM.Theme.getSize("default_margin").height + } + height: childrenRect.height + Repeater + { + model: printJob.compatibleMachineFamilies + + delegate: PrinterFamilyPill + { + text: modelData + color: UM.Theme.getColor("viewport_background") + padding: 3 + } + } + } + // PrintCore && Material config + Row + { + id: extrudersInfo + anchors.bottom: parent.bottom + + anchors + { + left: parent.left + right: parent.right + } + height: childrenRect.height + + spacing: UM.Theme.getSize("default_margin").width + + PrintCoreConfiguration + { + id: leftExtruderInfo + width: Math.round(parent.width / 2) + printCoreConfiguration: printJob.configuration.extruderConfigurations[0] + } + + PrintCoreConfiguration + { + id: rightExtruderInfo + width: Math.round(parent.width / 2) + printCoreConfiguration: printJob.configuration.extruderConfigurations[1] + } + } + + } + + Rectangle + { + color: UM.Theme.getColor("viewport_background") + width: 2 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").height + anchors.horizontalCenter: parent.horizontalCenter + } + } +} \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/PrinterFamilyPill.qml b/plugins/UM3NetworkPrinting/PrinterFamilyPill.qml new file mode 100644 index 0000000000..b785cd02b7 --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrinterFamilyPill.qml @@ -0,0 +1,28 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import UM 1.2 as UM + +Item +{ + property alias color: background.color + property alias text: familyNameLabel.text + property var padding: 0 + implicitHeight: familyNameLabel.contentHeight + 2 * padding // Apply the padding to top and bottom. + implicitWidth: familyNameLabel.contentWidth + implicitHeight // The extra height is added to ensure the radius doesn't cut something off. + Rectangle + { + id: background + height: parent.height + width: parent.width + color: parent.color + anchors.right: parent.right + anchors.horizontalCenter: parent.horizontalCenter + radius: 0.5 * height + } + Label + { + id: familyNameLabel + anchors.centerIn: parent + text: "" + } +} \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml index 68758e095e..74c8ec8483 100644 --- a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml +++ b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml @@ -7,6 +7,8 @@ import UM 1.3 as UM Item { + property var camera: null + Rectangle { anchors.fill:parent @@ -17,7 +19,7 @@ Item MouseArea { anchors.fill: parent - onClicked: OutputDevice.setActivePrinter(null) + onClicked: OutputDevice.setActiveCamera(null) z: 0 } @@ -32,7 +34,7 @@ Item width: 20 * screenScaleFactor height: 20 * screenScaleFactor - onClicked: OutputDevice.setActivePrinter(null) + onClicked: OutputDevice.setActiveCamera(null) style: ButtonStyle { @@ -65,23 +67,24 @@ Item { if(visible) { - if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) + if(camera != null) { - OutputDevice.activePrinter.camera.start() + camera.start() } } else { - if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) + if(camera != null) { - OutputDevice.activePrinter.camera.stop() + camera.stop() } } } + source: { - if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) + if(camera != null && camera.latestImage != null) { - return OutputDevice.activePrinter.camera.latestImage; + return camera.latestImage; } return ""; } @@ -92,7 +95,7 @@ Item anchors.fill: cameraImage onClicked: { - OutputDevice.setActivePrinter(null) + OutputDevice.setActiveCamera(null) } z: 1 } diff --git a/plugins/UM3NetworkPrinting/UM3-icon.svg b/plugins/UM3NetworkPrinting/UM3-icon.svg new file mode 100644 index 0000000000..6b5d4e4895 --- /dev/null +++ b/plugins/UM3NetworkPrinting/UM3-icon.svg @@ -0,0 +1 @@ +UM3-icon \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/UM3x-icon.svg b/plugins/UM3NetworkPrinting/UM3x-icon.svg new file mode 100644 index 0000000000..3708173dc5 --- /dev/null +++ b/plugins/UM3NetworkPrinting/UM3x-icon.svg @@ -0,0 +1 @@ +UM3x-icon \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/UMs5-icon.svg b/plugins/UM3NetworkPrinting/UMs5-icon.svg new file mode 100644 index 0000000000..78437465b3 --- /dev/null +++ b/plugins/UM3NetworkPrinting/UMs5-icon.svg @@ -0,0 +1 @@ +UMs5-icon \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/aborted-icon.svg b/plugins/UM3NetworkPrinting/aborted-icon.svg new file mode 100644 index 0000000000..7ef82c8911 --- /dev/null +++ b/plugins/UM3NetworkPrinting/aborted-icon.svg @@ -0,0 +1 @@ +aborted-icon \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/approved-icon.svg b/plugins/UM3NetworkPrinting/approved-icon.svg new file mode 100644 index 0000000000..671957d709 --- /dev/null +++ b/plugins/UM3NetworkPrinting/approved-icon.svg @@ -0,0 +1 @@ +approved-icon \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/paused-icon.svg b/plugins/UM3NetworkPrinting/paused-icon.svg new file mode 100644 index 0000000000..a66217d662 --- /dev/null +++ b/plugins/UM3NetworkPrinting/paused-icon.svg @@ -0,0 +1 @@ +paused-icon \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/warning-icon.svg b/plugins/UM3NetworkPrinting/warning-icon.svg new file mode 100644 index 0000000000..1e5359a5eb --- /dev/null +++ b/plugins/UM3NetworkPrinting/warning-icon.svg @@ -0,0 +1 @@ +warning-icon \ No newline at end of file diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 6ee33dd2f2..3a041ae499 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -274,7 +274,7 @@ Column elide: Text.ElideRight } - // Everthing for the extruder icon + // Everything for the extruder icon Item { id: extruderIconItem