diff --git a/cura/CuraSplashScreen.py b/cura/CuraSplashScreen.py index eedf1e3d53..f340c01623 100644 --- a/cura/CuraSplashScreen.py +++ b/cura/CuraSplashScreen.py @@ -10,7 +10,6 @@ from PyQt5.QtWidgets import QSplashScreen from UM.Resources import Resources from UM.Application import Application - class CuraSplashScreen(QSplashScreen): def __init__(self): super().__init__() @@ -61,7 +60,7 @@ class CuraSplashScreen(QSplashScreen): # draw version text font = QFont() # Using system-default font here - font.setPointSize(28) + font.setPixelSize(37) painter.setFont(font) painter.drawText(220, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0]) if len(version) > 1: @@ -81,7 +80,7 @@ class CuraSplashScreen(QSplashScreen): # draw message text if self._current_message: font = QFont() # Using system-default font here - font.setPointSize(10) + font.setPixelSize(13) pen = QPen() pen.setColor(QColor(255, 255, 255, 255)) painter.setPen(pen) @@ -107,5 +106,3 @@ class CuraSplashScreen(QSplashScreen): self._to_stop = True self._change_timer.stop() super().close() - - diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 05941530ca..2363f16b9d 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -12,11 +12,13 @@ UM.Dialog { title: catalog.i18nc("@title:window", "Open Project") - width: 500 - height: 400 + minimumWidth: 500 * screenScaleFactor + minimumHeight: 400 * screenScaleFactor + width: minimumWidth + height: minumumHeight - property int comboboxHeight: 15 - property int spacerHeight: 10 + property int comboboxHeight: 15 * screenScaleFactor + property int spacerHeight: 10 * screenScaleFactor onClosing: manager.notifyClosed() onVisibleChanged: @@ -31,7 +33,7 @@ UM.Dialog Item { anchors.fill: parent - anchors.margins: 20 + anchors.margins: 20 * screenScaleFactor UM.I18nCatalog { @@ -59,7 +61,7 @@ UM.Dialog Column { anchors.fill: parent - spacing: 2 + spacing: 2 * screenScaleFactor Label { id: titleLabel @@ -373,7 +375,7 @@ UM.Dialog enabled: true anchors.bottom: parent.bottom anchors.right: ok_button.left - anchors.rightMargin:2 + anchors.rightMargin: 2 * screenScaleFactor } Button { diff --git a/plugins/ImageReader/ConfigUI.qml b/plugins/ImageReader/ConfigUI.qml index 65374702ab..bcaf47b1fe 100644 --- a/plugins/ImageReader/ConfigUI.qml +++ b/plugins/ImageReader/ConfigUI.qml @@ -10,11 +10,11 @@ import UM 1.1 as UM UM.Dialog { - width: 350 * Screen.devicePixelRatio; - minimumWidth: 350 * Screen.devicePixelRatio; + width: minimumWidth; + minimumWidth: 350 * screenScaleFactor; - height: 250 * Screen.devicePixelRatio; - minimumHeight: 250 * Screen.devicePixelRatio; + height: minimumHeight; + minimumHeight: 250 * screenScaleFactor; title: catalog.i18nc("@title:window", "Convert Image...") @@ -23,8 +23,8 @@ UM.Dialog UM.I18nCatalog{id: catalog; name:"cura"} anchors.fill: parent; Layout.fillWidth: true - columnSpacing: 16 - rowSpacing: 4 + columnSpacing: 16 * screenScaleFactor + rowSpacing: 4 * screenScaleFactor columns: 1 UM.TooltipArea { @@ -36,7 +36,7 @@ UM.Dialog Label { text: catalog.i18nc("@action:label","Height (mm)") - width: 150 + width: 150 * screenScaleFactor anchors.verticalCenter: parent.verticalCenter } @@ -44,7 +44,7 @@ UM.Dialog id: peak_height objectName: "Peak_Height" validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;} - width: 180 + width: 180 * screenScaleFactor onTextChanged: { manager.onPeakHeightChanged(text) } } } @@ -59,7 +59,7 @@ UM.Dialog Label { text: catalog.i18nc("@action:label","Base (mm)") - width: 150 + width: 150 * screenScaleFactor anchors.verticalCenter: parent.verticalCenter } @@ -67,7 +67,7 @@ UM.Dialog id: base_height objectName: "Base_Height" validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;} - width: 180 + width: 180 * screenScaleFactor onTextChanged: { manager.onBaseHeightChanged(text) } } } @@ -82,7 +82,7 @@ UM.Dialog Label { text: catalog.i18nc("@action:label","Width (mm)") - width: 150 + width: 150 * screenScaleFactor anchors.verticalCenter: parent.verticalCenter } @@ -91,7 +91,7 @@ UM.Dialog objectName: "Width" focus: true validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;} - width: 180 + width: 180 * screenScaleFactor onTextChanged: { manager.onWidthChanged(text) } } } @@ -106,7 +106,7 @@ UM.Dialog Label { text: catalog.i18nc("@action:label","Depth (mm)") - width: 150 + width: 150 * screenScaleFactor anchors.verticalCenter: parent.verticalCenter } TextField { @@ -114,7 +114,7 @@ UM.Dialog objectName: "Depth" focus: true validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;} - width: 180 + width: 180 * screenScaleFactor onTextChanged: { manager.onDepthChanged(text) } } } @@ -130,14 +130,14 @@ UM.Dialog //Empty label so 2 column layout works. Label { text: "" - width: 150 + width: 150 * screenScaleFactor anchors.verticalCenter: parent.verticalCenter } ComboBox { id: image_color_invert objectName: "Image_Color_Invert" model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ] - width: 180 + width: 180 * screenScaleFactor onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) } } } @@ -152,13 +152,13 @@ UM.Dialog Label { text: catalog.i18nc("@action:label","Smoothing") - width: 150 + width: 150 * screenScaleFactor anchors.verticalCenter: parent.verticalCenter } Item { - width: 180 - height: 20 + width: 180 * screenScaleFactor + height: 20 * screenScaleFactor Layout.fillWidth: true Slider { diff --git a/plugins/LayerView/LayerView.qml b/plugins/LayerView/LayerView.qml index 21032be6ea..ecee766b2e 100755 --- a/plugins/LayerView/LayerView.qml +++ b/plugins/LayerView/LayerView.qml @@ -111,7 +111,7 @@ Item visible: !UM.LayerView.compatibilityMode style: UM.Theme.styles.combobox anchors.right: parent.right - anchors.rightMargin: 10 + anchors.rightMargin: 10 * screenScaleFactor onActivated: { @@ -353,7 +353,7 @@ Item property real minimumRangeHandleSize: UM.Theme.getSize("slider_handle").width / 2 property real trackThickness: UM.Theme.getSize("slider_groove").width property real trackRadius: trackThickness / 2 - property real trackBorderWidth: UM.Theme.getSize("default_lining").width / 2 + property real trackBorderWidth: UM.Theme.getSize("default_lining").width property color upperHandleColor: UM.Theme.getColor("slider_handle") property color lowerHandleColor: UM.Theme.getColor("slider_handle") property color rangeHandleColor: UM.Theme.getColor("slider_groove_fill") @@ -602,7 +602,7 @@ Item anchors.leftMargin: UM.Theme.getSize("default_margin").width / 2; anchors.verticalCenter: parent.verticalCenter; - width: Math.max(UM.Theme.getSize("line").width * maxValue.length + 2, 20); + width: Math.max(UM.Theme.getSize("line").width * maxValue.length + 2 * screenScaleFactor, 20 * screenScaleFactor); style: TextFieldStyle { textColor: UM.Theme.getColor("setting_control_text"); diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 832e247d19..3672066678 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -112,7 +112,13 @@ class MachineSettingsAction(MachineAction): if not self._global_container_stack: return 0 - return len(self._global_container_stack.getMetaDataEntry("machine_extruder_trains")) + # If there is a printer that originally is multi-extruder, it's not allowed to change the number of extruders + # It's just allowed in case of Custom FDM printers + definition_container = self._global_container_stack.getBottom() + if definition_container.getId() == "custom": + return len(self._global_container_stack.getMetaDataEntry("machine_extruder_trains")) + return 0 + @pyqtSlot(int) def setMachineExtruderCount(self, extruder_count): diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 28cd2759f6..89c082f19c 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -30,7 +30,7 @@ Item { { // This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar. // It kinda looks ugly otherwise (big panel, no content on it) - property int maximumHeight: 200 * Screen.devicePixelRatio + property int maximumHeight: 200 * screenScaleFactor height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight) ScrollView @@ -246,7 +246,7 @@ Item { id: settingPickDialog title: catalog.i18nc("@title:window", "Select Settings to Customize for this model") - width: Screen.devicePixelRatio * 360; + width: screenScaleFactor * 360; property string labelFilter: "" diff --git a/plugins/PluginBrowser/PluginBrowser.qml b/plugins/PluginBrowser/PluginBrowser.qml index f8a4001443..cbb60aed70 100644 --- a/plugins/PluginBrowser/PluginBrowser.qml +++ b/plugins/PluginBrowser/PluginBrowser.qml @@ -9,10 +9,10 @@ UM.Dialog id: base title: catalog.i18nc("@title:window", "Find & Update plugins") - width: 600 * Screen.devicePixelRatio - height: 450 * Screen.devicePixelRatio - minimumWidth: 350 * Screen.devicePixelRatio - minimumHeight: 350 * Screen.devicePixelRatio + width: 600 * screenScaleFactor + height: 450 * screenScaleFactor + minimumWidth: 350 * screenScaleFactor + minimumHeight: 350 * screenScaleFactor Item { anchors.fill: parent diff --git a/plugins/UM3NetworkPrinting/ClusterControlItem.qml b/plugins/UM3NetworkPrinting/ClusterControlItem.qml new file mode 100644 index 0000000000..6558720943 --- /dev/null +++ b/plugins/UM3NetworkPrinting/ClusterControlItem.qml @@ -0,0 +1,243 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 + +import UM 1.3 as UM +import Cura 1.0 as Cura + +Component +{ + Item + { + id: base + property var manager: Cura.MachineManager.printerOutputDevices[0] + anchors.fill: parent + property var lineColor: "#DCDCDC" // TODO: Should be linked to theme. + property var cornerRadius: 4 // TODO: Should be linked to theme. + + visible: manager != null + + UM.I18nCatalog + { + id: catalog + name: "cura" + } + + Label + { + id: activePrintersLabel + font: UM.Theme.getFont("large") + anchors.horizontalCenter: parent.horizontalCenter + text: Cura.MachineManager.printerOutputDevices[0].name + } + Label + { + id: printerGroupLabel + anchors.top: activePrintersLabel.bottom + text: catalog.i18nc("@label", "PRINTER GROUP") + anchors.horizontalCenter: parent.horizontalCenter + font: UM.Theme.getFont("very_small") + opacity: 0.65 + } + + Rectangle + { + id: printJobArea + border.width: UM.Theme.getSize("default_lining").width + border.color: lineColor + anchors.top: printerGroupLabel.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 + //TODO; It's probably nicer to do this with a dynamic data model instead of hardcoding this. + //But you know the drill; time constraints don't result in elegant code. + 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.numJobsPrinting + 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.numJobsQueued + 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 + } + } + + + Rectangle + { + 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 + + Item + { + 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 + } + } + 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 + { + model: manager.connectedPrintersTypeCount + Item + { + width: parent.width + height: childrenRect.height + opacity: 0.65 + Label + { + text: modelData.machine_type + font: UM.Theme.getFont("very_small") + } + + Label + { + text: modelData.count + font: UM.Theme.getFont("small") + anchors.right: parent.right + } + } + } + } + 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 + } + } + } +} \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml new file mode 100644 index 0000000000..d39cdab81e --- /dev/null +++ b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml @@ -0,0 +1,108 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.3 as UM +import Cura 1.0 as Cura + +Component +{ + Rectangle + { + width: maximumWidth + height: maximumHeight + color: "#FFFFFF" // TODO; Should not be hardcoded. + + property var emphasisColor: "#44c0ff" //TODO: should be linked to theme. + property var lineColor: "#DCDCDC" // TODO: Should be linked to theme. + property var cornerRadius: 4 // TODO: Should be linked to theme. + UM.I18nCatalog + { + id: catalog + name: "cura" + } + + Label + { + id: activePrintersLabel + font: UM.Theme.getFont("large") + + text: + { + if (OutputDevice.connectedPrinters.length == 0){ + return 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) + } else { + return "" + } + } + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: UM.Theme.getSize("default_margin").height + + visible: OutputDevice.connectedPrinters.length == 0 + } + + Item + { + anchors.topMargin: UM.Theme.getSize("default_margin").height + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + + width: Math.min(800, maximumWidth) + height: children.height + visible: OutputDevice.connectedPrinters.length != 0 + + Label + { + id: addRemovePrintersLabel + anchors.right: parent.right + text: "Add / remove printers" + } + + MouseArea + { + anchors.fill: addRemovePrintersLabel + onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel() + } + } + + + 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 + + ListView + { + anchors.fill: parent + spacing: -UM.Theme.getSize("default_lining").height + + model: OutputDevice.connectedPrinters + + delegate: PrinterInfoBlock + { + printer: modelData + width: Math.min(800, maximumWidth) + height: 125 + + // Add a 1 pix margin, as the border is sometimes cut off otherwise. + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + PrinterVideoStream + { + visible: OutputDevice.selectedPrinterName != "" + anchors.fill:parent + } + } +} diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index 58f155533f..5ea7d77d8a 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -272,6 +272,28 @@ Cura.MachineAction text: base.selectedPrinter ? base.selectedPrinter.ipAddress : "" } } + + Label + { + width: parent.width + wrapMode: Text.WordWrap + text:{ + // The property cluster size does not exist for older UM3 devices. + if(base.selectedPrinter.clusterSize == null || base.selectedPrinter.clusterSize == 1) + { + return ""; + } + else if (base.selectedPrinter.clusterSize === 0) + { + return catalog.i18nc("@label", "Cura Connect: This printer is not set up to host a group of connected Ultimaker 3 printers."); + } + else + { + return catalog.i18nc("@label", "Cura Connect: This printer is set up to host a group of %1 connected Ultimaker 3 printers".arg(base.selectedPrinter.clusterSize)); + } + } + + } Label { width: parent.width @@ -298,8 +320,8 @@ Cura.MachineAction title: catalog.i18nc("@title:window", "Printer Address") - minimumWidth: 400 * Screen.devicePixelRatio - minimumHeight: 120 * Screen.devicePixelRatio + minimumWidth: 400 * screenScaleFactor + minimumHeight: 120 * screenScaleFactor width: minimumWidth height: minimumHeight diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py new file mode 100644 index 0000000000..55f9d1247b --- /dev/null +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -0,0 +1,638 @@ +import datetime +import getpass +import gzip +import json +import os +import os.path +import time + +from enum import Enum +from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart +from PyQt5.QtCore import QUrl, QByteArray, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply +from PyQt5.QtQml import QQmlComponent, QQmlContext +from UM.Application import Application +from UM.Logger import Logger +from UM.Message import Message +from UM.OutputDevice import OutputDeviceError +from UM.i18n import i18nCatalog + +from . import NetworkPrinterOutputDevice + + +i18n_catalog = i18nCatalog("cura") + + +class OutputStage(Enum): + ready = 0 + uploading = 2 + + +class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice): + printJobsChanged = pyqtSignal() + printersChanged = pyqtSignal() + selectedPrinterChanged = pyqtSignal() + + def __init__(self, key, address, properties, api_prefix, plugin_path): + super().__init__(key, address, properties, api_prefix) + # Store the address of the master. + self._master_address = address + name_property = properties.get(b"name", b"") + if name_property: + name = name_property.decode("utf-8") + else: + name = key + + self._plugin_path = plugin_path + + self.setName(name) + description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network") + self.setShortDescription(description) + self.setDescription(description) + + self._stage = OutputStage.ready + host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "") + if host_override: + Logger.log( + "w", + "Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host", + host_override) + self._host = "http://" + host_override + else: + self._host = "http://" + address + + # is the same as in NetworkPrinterOutputDevicePlugin + self._cluster_api_version = "1" + self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" + self._api_base_uri = self._host + self._cluster_api_prefix + + self._file_name = None + self._progress_message = None + self._request = None + self._reply = None + + # The main reason to keep the 'multipart' form data on the object + # is to prevent the Python GC from claiming it too early. + self._multipart = None + + self._print_view = None + self._request_job = [] + + self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") + self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") + + self._print_jobs = [] + self._print_job_by_printer_uuid = {} + self._print_job_by_uuid = {} # Print jobs by their own uuid + self._printers = [] + self._printers_dict = {} # by unique_name + + self._connected_printers_type_count = [] + self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"} # empty unique_name IS automatic selection + self._selected_printer = self._automatic_printer + + self._cluster_status_update_timer = QTimer() + self._cluster_status_update_timer.setInterval(5000) + self._cluster_status_update_timer.setSingleShot(False) + self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus) + + self._can_pause = False + self._can_abort = False + self._can_pre_heat_bed = False + self._cluster_size = int(properties.get(b"cluster_size", 0)) + + self._cleanupRequest() + + #These are texts that are to be translated for future features. + temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.") + temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3) + temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished. + temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed. + + @pyqtProperty(QObject, notify=selectedPrinterChanged) + def controlItem(self): + # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. + if not self._control_component: + self._createControlViewFromQML() + name = self._selected_printer.get("friendly_name") + if name == self._automatic_printer.get("friendly_name") or name == "": + return self._control_item + # Let cura use the default. + return None + + @pyqtSlot(int, result = str) + def getTimeCompleted(self, time_remaining): + current_time = time.time() + datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) + return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute) + + @pyqtSlot(int, result = str) + def getDateCompleted(self, time_remaining): + current_time = time.time() + datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) + return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() + + @pyqtProperty(int, constant = True) + def clusterSize(self): + return self._cluster_size + + @pyqtProperty(str, notify=selectedPrinterChanged) + def name(self): + # Show the name of the selected printer. + # This is not the nicest way to do this, but changes to the Cura UI are required otherwise. + name = self._selected_printer.get("friendly_name") + if name != self._automatic_printer.get("friendly_name"): + return name + # Return name of cluster master. + return self._properties.get(b"name", b"").decode("utf-8") + + def connect(self): + super().connect() + self._cluster_status_update_timer.start() + + def close(self): + super().close() + self._cluster_status_update_timer.stop() + + def _requestClusterStatus(self): + # TODO: Handle timeout. We probably want to know if the cluster is still reachable or not. + url = QUrl(self._api_base_uri + "print_jobs/") + print_jobs_request = QNetworkRequest(url) + self._addUserAgentHeader(print_jobs_request) + self._manager.get(print_jobs_request) + # See _finishedPrintJobsRequest() + + url = QUrl(self._api_base_uri + "printers/") + printers_request = QNetworkRequest(url) + self._addUserAgentHeader(printers_request) + self._manager.get(printers_request) + # See _finishedPrintersRequest() + + def _finishedPrintJobsRequest(self, reply): + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + except json.decoder.JSONDecodeError: + Logger.log("w", "Received an invalid print job state message: Not valid JSON.") + return + self.setPrintJobs(json_data) + + def _finishedPrintersRequest(self, reply): + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + except json.decoder.JSONDecodeError: + Logger.log("w", "Received an invalid print job state message: Not valid JSON.") + return + self.setPrinters(json_data) + + def materialHotendChangedMessage(self, callback): + pass # Do nothing. + + def _startCameraStream(self): + ## Request new image + url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream") + self._image_request = QNetworkRequest(url) + self._addUserAgentHeader(self._image_request) + self._image_reply = self._manager.get(self._image_request) + self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) + + def spawnPrintView(self): + if self._print_view is None: + path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml")) + component = QQmlComponent(Application.getInstance()._engine, path) + + self._print_context = QQmlContext(Application.getInstance()._engine.rootContext()) + self._print_context.setContextProperty("OutputDevice", self) + self._print_view = component.create(self._print_context) + + if component.isError(): + Logger.log("e", " Errors creating component: \n%s", "\n".join( + [e.toString() for e in component.errors()])) + + if self._print_view is not None: + self._print_view.show() + + ## Store job info, show Print view for settings + def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): + self._selected_printer = self._automatic_printer # reset to default option + self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] + + if self._stage != OutputStage.ready: + if self._error_message: + self._error_message.hide() + self._error_message = Message( + i18n_catalog.i18nc("@info:status", + "Sending new jobs (temporarily) blocked, still sending the previous print job.")) + self._error_message.show() + return + + if len(self._printers) > 1: + self.spawnPrintView() # Ask user how to print it. + elif len(self._printers) == 1: + # If there is only one printer, don't bother asking. + self.selectAutomaticPrinter() + self.sendPrintJob() + else: + # Cluster has no printers, warn the user of this. + if self._error_message: + self._error_message.hide() + self._error_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers.")) + self._error_message.show() + + ## Actually send the print job, called from the dialog + # :param: require_printer_name: name of printer, or "" + @pyqtSlot() + def sendPrintJob(self): + nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job + require_printer_name = self._selected_printer["unique_name"] + + self._send_gcode_start = time.time() + Logger.log("d", "Sending print job [%s] to host..." % file_name) + + if self._stage != OutputStage.ready: + Logger.log("d", "Unable to send print job as the state is %s", self._stage) + raise OutputDeviceError.DeviceBusyError() + self._stage = OutputStage.uploading + + self._file_name = "%s.gcode.gz" % file_name + self._showProgressMessage() + + self._request = self._buildSendPrintJobHttpRequest(require_printer_name) + self._reply = self._manager.post(self._request, self._multipart) + self._reply.uploadProgress.connect(self._onUploadProgress) + # See _finishedPostPrintJobRequest() + + def _buildSendPrintJobHttpRequest(self, require_printer_name): + api_url = QUrl(self._api_base_uri + "print_jobs/") + request = QNetworkRequest(api_url) + # Create multipart request and add the g-code. + self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) + + # Add gcode + part = QHttpPart() + part.setHeader(QNetworkRequest.ContentDispositionHeader, + 'form-data; name="file"; filename="%s"' % self._file_name) + + gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") + compressed_gcode = self._compressGcode(gcode) + if compressed_gcode is None: + return # User aborted print, so stop trying. + + part.setBody(compressed_gcode) + self._multipart.append(part) + + # require_printer_name "" means automatic + if require_printer_name: + self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name)) + user_name = self.__get_username() + if user_name is None: + user_name = "unknown" + self._multipart.append(self.__createKeyValueHttpPart("owner", user_name)) + + self._addUserAgentHeader(request) + return request + + def _compressGcode(self, gcode): + self._compressing_print = True + batched_line = "" + max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB + + byte_array_file_data = b"" + + def _compressDataAndNotifyQt(data_to_append): + compressed_data = gzip.compress(data_to_append.encode("utf-8")) + QCoreApplication.processEvents() # Ensure that the GUI does not freeze. + # Pretend that this is a response, as zipping might take a bit of time. + self._last_response_time = time.time() + return compressed_data + + if gcode is None: + Logger.log("e", "Unable to find sliced gcode, returning empty.") + return byte_array_file_data + + for line in gcode: + if not self._compressing_print: + self._progress_message.hide() + return # Stop trying to zip, abort was called. + batched_line += line + # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file. + # Compressing line by line in this case is extremely slow, so we need to batch them. + if len(batched_line) < max_chars_per_line: + continue + byte_array_file_data += _compressDataAndNotifyQt(batched_line) + batched_line = "" + + # Also compress the leftovers. + if batched_line: + byte_array_file_data += _compressDataAndNotifyQt(batched_line) + + return byte_array_file_data + + def __createKeyValueHttpPart(self, key, value): + metadata_part = QHttpPart() + metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain') + metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key)) + metadata_part.setBody(bytearray(value, "utf8")) + return metadata_part + + def __get_username(self): + try: + return getpass.getuser() + except: + Logger.log("d", "Could not get the system user name, returning 'unknown' instead.") + return None + + def _finishedPrintJobPostRequest(self, reply): + self._stage = OutputStage.ready + if self._progress_message: + self._progress_message.hide() + self._progress_message = None + self.writeFinished.emit(self) + + if reply.error(): + self._showRequestFailedMessage(reply) + self.writeError.emit(self) + else: + self._showRequestSucceededMessage() + self.writeSuccess.emit(self) + + self._cleanupRequest() + + def _showRequestFailedMessage(self, reply): + if reply is not None: + Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format( + cluster_name = self.getName(), + error_string = str(reply.errorString()), + error = str(reply.error()))) + error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.") + message = Message(text=error_message_template.format( + cluster_name = self.getName())) + message.show() + + def _showRequestSucceededMessage(self): + confirmation_message_template = i18n_catalog.i18nc( + "@info:status", + "Sent {file_name} to group {cluster_name}." + ) + file_name = os.path.basename(self._file_name).split(".")[0] + message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name) + message = Message(text=message_text) + button_text = i18n_catalog.i18nc("@action:button", "Show print jobs") + button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.") + message.addAction("open_browser", button_text, "globe", button_tooltip) + message.actionTriggered.connect(self._onMessageActionTriggered) + message.show() + + def setPrintJobs(self, print_jobs): + #TODO: hack, last seen messes up the check, so drop it. + for job in print_jobs: + del job["last_seen"] + # Strip any extensions + job["name"] = self._removeGcodeExtension(job["name"]) + + if self._print_jobs != print_jobs: + old_print_jobs = self._print_jobs + self._print_jobs = print_jobs + + self._notifyFinishedPrintJobs(old_print_jobs, print_jobs) + + # Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer + # for some reason. ugh. + self._print_job_by_printer_uuid = {} + self._print_job_by_uuid = {} + for print_job in print_jobs: + if "printer_uuid" in print_job and print_job["printer_uuid"] is not None: + self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job + self._print_job_by_uuid[print_job["uuid"]] = print_job + self.printJobsChanged.emit() + + def _removeGcodeExtension(self, name): + parts = name.split(".") + if parts[-1].upper() == "GZ": + parts = parts[:-1] + if parts[-1].upper() == "GCODE": + parts = parts[:-1] + return ".".join(parts) + + def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs): + """Notify the user when any of their print jobs have just completed. + + Arguments: + + old_print_jobs -- the previous list of print job status information as returned by the cluster REST API. + new_print_jobs -- the current list of print job status information as returned by the cluster REST API. + """ + if old_print_jobs is None: + return + + username = self.__get_username() + if username is None: + return + + our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs) + our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"] + + our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs) + our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"] + + old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs]) + + for print_job in our_new_finished_print_jobs: + if print_job["uuid"] in old_not_finished_print_job_uuids: + + printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"]) + if printer_name is None: + printer_name = i18n_catalog.i18nc("@info:status", "Unknown printer") + + message_text = (i18n_catalog.i18nc("@info:status", + "Printer '{printer_name}' has finished printing '{job_name}'.") + .format(printer_name=printer_name, job_name=print_job["name"])) + message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished")) + Application.getInstance().showMessage(message) + Application.getInstance().showToastMessage( + i18n_catalog.i18nc("@info:status", "Print finished"), + message_text) + + def __filterOurPrintJobs(self, print_jobs): + username = self.__get_username() + return [print_job for print_job in print_jobs if print_job["owner"] == username] + + def __getPrinterNameFromUuid(self, printer_uuid): + for printer in self._printers: + if printer["uuid"] == printer_uuid: + return printer["friendly_name"] + return None + + def setPrinters(self, printers): + if self._printers != printers: + self._connected_printers_type_count = [] + printers_count = {} + self._printers = printers + self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name + + for printer in printers: + variant = printer["machine_variant"] + if variant in printers_count: + printers_count[variant] += 1 + else: + printers_count[variant] = 1 + for type in printers_count: + self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]}) + self.printersChanged.emit() + + @pyqtProperty("QVariantList", notify=printersChanged) + def connectedPrintersTypeCount(self): + return self._connected_printers_type_count + + @pyqtProperty("QVariantList", notify=printersChanged) + def connectedPrinters(self): + return self._printers + + @pyqtProperty(int, notify=printJobsChanged) + def numJobsPrinting(self): + num_jobs_printing = 0 + for job in self._print_jobs: + if job["status"] == "printing": + num_jobs_printing += 1 + return num_jobs_printing + + @pyqtProperty(int, notify=printJobsChanged) + def numJobsQueued(self): + num_jobs_queued = 0 + for job in self._print_jobs: + if job["status"] == "queued": + num_jobs_queued += 1 + return num_jobs_queued + + @pyqtProperty("QVariantMap", notify=printJobsChanged) + def printJobsByUUID(self): + return self._print_job_by_uuid + + @pyqtProperty("QVariantMap", notify=printJobsChanged) + def printJobsByPrinterUUID(self): + return self._print_job_by_printer_uuid + + @pyqtProperty("QVariantList", notify=printJobsChanged) + def printJobs(self): + return self._print_jobs + + @pyqtProperty("QVariantList", notify=printersChanged) + def printers(self): + return [self._automatic_printer, ] + self._printers + + @pyqtSlot(str, str) + def selectPrinter(self, unique_name, friendly_name): + self.stopCamera() + self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name} + Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name) + # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. + if unique_name == "": + self._address = self._master_address + else: + self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + + self.selectedPrinterChanged.emit() + + def _updateJobState(self, job_state): + name = self._selected_printer.get("friendly_name") + if name == "" or name == "Automatic": + # TODO: This is now a bit hacked; If no printer is selected, don't show job state. + if self._job_state != "": + self._job_state = "" + self.jobStateChanged.emit() + else: + if self._job_state != job_state: + self._job_state = job_state + self.jobStateChanged.emit() + + @pyqtSlot() + def selectAutomaticPrinter(self): + self.stopCamera() + self._selected_printer = self._automatic_printer + self.selectedPrinterChanged.emit() + + @pyqtProperty("QVariant", notify=selectedPrinterChanged) + def selectedPrinterName(self): + return self._selected_printer.get("unique_name", "") + + def getPrintJobsUrl(self): + return self._host + "/print_jobs" + + def getPrintersUrl(self): + return self._host + "/printers" + + def _showProgressMessage(self): + progress_message_template = i18n_catalog.i18nc("@info:progress", + "Sending {file_name} to group {cluster_name}") + file_name = os.path.basename(self._file_name).split(".")[0] + self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1) + self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") + self._progress_message.actionTriggered.connect(self._onMessageActionTriggered) + self._progress_message.show() + + def _addUserAgentHeader(self, request): + request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin") + + def _cleanupRequest(self): + self._reply = None + self._request = None + self._multipart = None + self._stage = OutputStage.ready + self._file_name = None + + def _onFinished(self, reply): + super()._onFinished(reply) + reply_url = reply.url().toString() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + if status_code == 500: + Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url)) + return + if reply.error() == QNetworkReply.ContentOperationNotPermittedError: + # It was probably "/api/v1/materials" for legacy UM3 + return + if reply.error() == QNetworkReply.ContentNotFoundError: + # It was probably "/api/v1/print_job" for legacy UM3 + return + + if reply.operation() == QNetworkAccessManager.PostOperation: + if self._cluster_api_prefix + "print_jobs" in reply_url: + self._finishedPrintJobPostRequest(reply) + return + + # We need to do this check *after* we process the post operation! + # If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this. + if reply.error() != QNetworkReply.NoError: + Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error()) + return + + elif reply.operation() == QNetworkAccessManager.GetOperation: + if self._cluster_api_prefix + "print_jobs" in reply_url: + self._finishedPrintJobsRequest(reply) + elif self._cluster_api_prefix + "printers" in reply_url: + self._finishedPrintersRequest(reply) + + @pyqtSlot() + def openPrintJobControlPanel(self): + Logger.log("d", "Opening print job control panel...") + QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) + + @pyqtSlot() + def openPrinterControlPanel(self): + Logger.log("d", "Opening printer control panel...") + QDesktopServices.openUrl(QUrl(self.getPrintersUrl())) + + def _onMessageActionTriggered(self, message, action): + if action == "open_browser": + QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) + + if action == "Abort": + Logger.log("d", "User aborted sending print to remote.") + self._progress_message.hide() + self._compressing_print = False + self._stage = OutputStage.ready + if self._reply: + self._reply.abort() + self._reply = None + Application.getInstance().showPrintMonitor.emit(False) diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py index 8c7a07ef4b..44ac965eae 100755 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py @@ -17,7 +17,7 @@ import cura.Settings.ExtruderManager from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot, QCoreApplication -from PyQt5.QtGui import QImage +from PyQt5.QtGui import QImage, QColor from PyQt5.QtWidgets import QMessageBox import json @@ -102,7 +102,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._target_bed_temperature = 0 self._processing_preheat_requests = True - self.setPriority(2) # Make sure the output device gets selected above local file output + self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) @@ -1007,7 +1007,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): reply_url = reply.url().toString() if reply.operation() == QNetworkAccessManager.GetOperation: - if "printer" in reply_url: # Status update from printer. + # "printer" is also in "printers", therefore _api_prefix is added. + if self._api_prefix + "printer" in reply_url: # Status update from printer. if status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) @@ -1025,7 +1026,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors - elif "print_job" in reply_url: # Status update from print_job: + elif self._api_prefix + "print_job" in reply_url: # Status update from print_job: if status_code == 200: try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py index 5f2ed1badc..39e5faf938 100644 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py @@ -1,26 +1,31 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin -from . import NetworkPrinterOutputDevice - -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore -from UM.Logger import Logger -from UM.Signal import Signal, signalemitter -from UM.Application import Application -from UM.Preferences import Preferences - -from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import QUrl - +import os import time import json +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply +from PyQt5.QtQml import QQmlComponent, QQmlContext +from UM.Application import Application +from UM.Logger import Logger +from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin +from UM.PluginRegistry import PluginRegistry +from UM.Preferences import Preferences +from UM.Signal import Signal, signalemitter +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore + +from . import NetworkPrinterOutputDevice, NetworkClusterPrinterOutputDevice + + ## This plugin handles the connection detection & creation of output device objects for the UM3 printer. # Zero-Conf is used to detect printers, which are saved in a dict. # If we discover a printer that has the same key as the active machine instance a connection is made. @signalemitter -class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): +class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin): def __init__(self): super().__init__() self._zero_conf = None @@ -29,6 +34,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" + self._cluster_api_version = "1" + self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" self._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onNetworkRequestFinished) @@ -47,6 +54,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",") + self._network_requests_buffer = {} # store api responses until data is complete + addPrinterSignal = Signal() removePrinterSignal = Signal() printerListChanged = Signal() @@ -91,6 +100,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.addPrinter(instance_name, address, properties) self.checkManualPrinter(address) + self.checkClusterPrinter(address) def removeManualPrinter(self, key, address = None): if key in self._printers: @@ -105,18 +115,26 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def checkManualPrinter(self, address): # Check if a printer exists at this address # If a printer responds, it will replace the preliminary printer created above - url = QUrl("http://" + address + self._api_prefix + "system") + # origin=manual is for tracking back the origin of the call + url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name") name_request = QNetworkRequest(url) self._network_manager.get(name_request) + def checkClusterPrinter(self, address): + cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster") + cluster_request = QNetworkRequest(cluster_url) + self._network_manager.get(cluster_request) + ## Handler for all requests that have finished. def _onNetworkRequestFinished(self, reply): reply_url = reply.url().toString() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: - if "system" in reply_url: # Name returned from printer. + address = reply.url().host() + if "origin=manual_name" in reply_url: # Name returned from printer. if status_code == 200: + try: system_info = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.JSONDecodeError: @@ -125,28 +143,51 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): except UnicodeDecodeError: Logger.log("e", "Printer returned incorrect UTF-8.") return - address = reply.url().host() - instance_name = "manual:%s" % address - machine = "unknown" - if "variant" in system_info: - variant = system_info["variant"] - if variant == "Ultimaker 3": - machine = "9066" - elif variant == "Ultimaker 3 Extended": - machine = "9511" - properties = { - b"name": system_info["name"].encode("utf-8"), - b"address": address.encode("utf-8"), - b"firmware_version": system_info["firmware"].encode("utf-8"), - b"manual": b"true", - b"machine": machine.encode("utf-8") - } - if instance_name in self._printers: - # Only replace the printer if it is still in the list of (manual) printers - self.removePrinter(instance_name) - self.addPrinter(instance_name, address, properties) + if address not in self._network_requests_buffer: + self._network_requests_buffer[address] = {} + self._network_requests_buffer[address]["system"] = system_info + elif "origin=check_cluster" in reply_url: + if address not in self._network_requests_buffer: + self._network_requests_buffer[address] = {} + if status_code == 200: + # We know it's a cluster printer + Logger.log("d", "Cluster printer detected: [%s]", reply.url()) + self._network_requests_buffer[address]["cluster"] = True + else: + Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url()) + self._network_requests_buffer[address]["cluster"] = False + + # Both the system call and cluster call are finished + if (address in self._network_requests_buffer and + "system" in self._network_requests_buffer[address] and + "cluster" in self._network_requests_buffer[address]): + + instance_name = "manual:%s" % address + system_info = self._network_requests_buffer[address]["system"] + is_cluster = self._network_requests_buffer[address]["cluster"] + machine = "unknown" + if "variant" in system_info: + variant = system_info["variant"] + if variant == "Ultimaker 3": + machine = "9066" + elif variant == "Ultimaker 3 Extended": + machine = "9511" + + properties = { + b"name": system_info["name"].encode("utf-8"), + b"address": address.encode("utf-8"), + b"firmware_version": system_info["firmware"].encode("utf-8"), + b"manual": b"true", + b"machine": machine.encode("utf-8") + } + if instance_name in self._printers: + # Only replace the printer if it is still in the list of (manual) printers + self.removePrinter(instance_name) + self.addPrinter(instance_name, address, properties, force_cluster=is_cluster) + + del self._network_requests_buffer[address] ## Stop looking for devices on network. def stop(self): @@ -175,8 +216,13 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged) ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - def addPrinter(self, name, address, properties): - printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix) + def addPrinter(self, name, address, properties, force_cluster=False): + cluster_size = int(properties.get(b"cluster_size", -1)) + if force_cluster or cluster_size >= 0: + printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice( + name, address, properties, self._api_prefix, self._plugin_path) + else: + printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix) self._printers[printer.getKey()] = printer global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"): @@ -237,4 +283,22 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): elif state_change == ServiceStateChange.Removed: Logger.log("d", "Bonjour service removed: %s" % name) - self.removePrinterSignal.emit(str(name)) \ No newline at end of file + self.removePrinterSignal.emit(str(name)) + + ## For cluster below + def _get_plugin_directory_name(self): + current_file_absolute_path = os.path.realpath(__file__) + directory_path = os.path.dirname(current_file_absolute_path) + _, directory_name = os.path.split(directory_path) + return directory_name + + @property + def _plugin_path(self): + return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name()) + + @pyqtSlot() + def openControlPanel(self): + Logger.log("d", "Opening print jobs web UI...") + selected_device = self.getOutputDeviceManager().getActiveDevice() + if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice): + QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl())) diff --git a/plugins/UM3NetworkPrinting/OpenPanelButton.qml b/plugins/UM3NetworkPrinting/OpenPanelButton.qml new file mode 100644 index 0000000000..3915c1f9eb --- /dev/null +++ b/plugins/UM3NetworkPrinting/OpenPanelButton.qml @@ -0,0 +1,18 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 + +import UM 1.1 as UM + +Button { + objectName: "openPanelSaveAreaButton" + id: openPanelSaveAreaButton + + UM.I18nCatalog { id: catalog; name: "cura"; } + + height: UM.Theme.getSize("save_button_save_to_button").height + tooltip: catalog.i18nc("@info:tooltip", "Opens the print jobs page with your default web browser.") + text: catalog.i18nc("@action:button", "View print jobs") + + style: UM.Theme.styles.sidebar_action_button +} diff --git a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml new file mode 100644 index 0000000000..624c02f735 --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml @@ -0,0 +1,33 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.2 as UM + + +Item +{ + id: extruderInfo + property var printCoreConfiguration + + width: parent.width / 2 + height: childrenRect.height + Label + { + id: materialLabel + text: printCoreConfiguration.material.material + " (" + printCoreConfiguration.material.color + ")" + elide: Text.ElideRight + width: parent.width + font: UM.Theme.getFont("very_small") + } + Label + { + id: printCoreLabel + text: printCoreConfiguration.print_core_id + anchors.top: materialLabel.bottom + elide: Text.ElideRight + width: parent.width + font: UM.Theme.getFont("very_small") + opacity: 0.5 + } +} diff --git a/plugins/UM3NetworkPrinting/PrintWindow.qml b/plugins/UM3NetworkPrinting/PrintWindow.qml new file mode 100644 index 0000000000..28e8a72160 --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrintWindow.qml @@ -0,0 +1,103 @@ +// Copyright (c) 2015 Ultimaker B.V. +// Cura is released under the terms of the AGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Window 2.2 +import QtQuick.Controls 1.2 + +import UM 1.1 as UM + +UM.Dialog +{ + id: base; + + minimumWidth: 500 + minimumHeight: 140 + maximumWidth: minimumWidth + maximumHeight: minimumHeight + width: minimumWidth + height: minimumHeight + + visible: true + modality: Qt.ApplicationModal + + title: catalog.i18nc("@title:window","Print over network") + + Column + { + id: printerSelection + anchors.fill: parent + anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("default_margin").height + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.rightMargin: UM.Theme.getSize("default_margin").width + height: 50 + + Label + { + id: manualPrinterSelectionLabel + anchors + { + left: parent.left + topMargin: UM.Theme.getSize("default_margin").height + right: parent.right + } + text: "Printer selection" + wrapMode: Text.Wrap + height: 20 + } + + ComboBox + { + id: printerSelectionCombobox + model: OutputDevice.printers + textRole: "friendly_name" + + width: parent.width + height: 40 + Behavior on height { NumberAnimation { duration: 100 } } + + onActivated: + { + var printerData = OutputDevice.printers[index]; + OutputDevice.selectPrinter(printerData.unique_name, printerData.friendly_name); + } + } + + SystemPalette + { + id: palette + } + + UM.I18nCatalog { id: catalog; name: "cura"; } + } + + leftButtons: [ + Button + { + text: catalog.i18nc("@action:button","Cancel") + enabled: true + onClicked: { + base.visible = false; + // reset to defaults + OutputDevice.selectAutomaticPrinter() + printerSelectionCombobox.currentIndex = 0 + } + } + ] + + rightButtons: [ + Button + { + text: catalog.i18nc("@action:button","Print") + enabled: true + onClicked: { + base.visible = false; + OutputDevice.sendPrintJob(); + // reset to defaults + OutputDevice.selectAutomaticPrinter() + printerSelectionCombobox.currentIndex = 0 + } + } + ] +} diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml new file mode 100644 index 0000000000..bab7db41d4 --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -0,0 +1,345 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.3 as UM + + +Rectangle +{ + function strPadLeft(string, pad, length) + { + return (new Array(length + 1).join(pad) + string).slice(-length); + } + + function getPrettyTime(time) + { + var hours = Math.floor(time / 3600) + time -= hours * 3600 + var minutes = Math.floor(time / 60); + time -= minutes * 60 + var seconds = Math.floor(time); + + var finalTime = strPadLeft(hours, "0", 2) + ':' + strPadLeft(minutes,'0',2)+ ':' + strPadLeft(seconds,'0',2); + return finalTime; + } + + function formatPrintJobPercent(printJob) + { + if (printJob == null) + { + return ""; + } + if (printJob.time_total === 0) + { + return ""; + } + return Math.min(100, Math.round(printJob.time_elapsed / printJob.time_total * 100)) + "%"; + } + + + id: printerDelegate + property var printer + + border.width: UM.Theme.getSize("default_lining").width + border.color: mouse.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : lineColor + z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible. + + property var printJob: + { + if (printer.reserved_by != null) + { + // Look in another list. + return OutputDevice.printJobsByUUID[printer.reserved_by] + } + return OutputDevice.printJobsByPrinterUUID[printer.uuid] + } + + MouseArea + { + id: mouse + anchors.fill:parent + onClicked: OutputDevice.selectPrinter(printer.unique_name, printer.friendly_name) + hoverEnabled: true; + + // Only clickable if no printer is selected + enabled: OutputDevice.selectedPrinterName == "" + } + + Row + { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").width + + Rectangle + { + width: parent.width / 3 + height: parent.height + + Label // Print job name + { + id: jobNameLabel + anchors.top: parent.top + anchors.left: parent.left + text: printJob != null ? printJob.name : "" + font: UM.Theme.getFont("default_bold") + } + + Label + { + id: jobOwnerLabel + anchors.top: jobNameLabel.bottom + text: printJob != null ? printJob.owner : "" + opacity: 0.50 + } + + Label + { + id: totalTimeLabel + anchors.bottom: parent.bottom + text: printJob != null ? getPrettyTime(printJob.time_total) : "" + opacity: 0.65 + font: UM.Theme.getFont("default") + } + } + + Rectangle + { + width: parent.width / 3 * 2 + height: parent.height + + Label // Friendly machine name + { + id: printerNameLabel + anchors.top: parent.top + anchors.left: parent.left + width: parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width + text: printer.friendly_name + font: UM.Theme.getFont("default_bold") + elide: Text.ElideRight + } + + Label // Machine variant + { + id: printerTypeLabel + anchors.top: printerNameLabel.bottom + width: parent.width / 2 - UM.Theme.getSize("default_margin").width + text: printer.machine_variant + anchors.left: parent.left + elide: Text.ElideRight + font: UM.Theme.getFont("very_small") + opacity: 0.50 + } + + Rectangle // Camera icon + { + id: showCameraIcon + width: 40 + height: width + radius: width + anchors.right: printProgressArea.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width + color: emphasisColor + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + source: "camera-icon.svg" + width: sourceSize.width + height: sourceSize.height * width / sourceSize.width + color: "white" + } + } + + Row // PrintCode config + { + id: extruderInfo + anchors.bottom: parent.bottom + + width: parent.width / 2 - UM.Theme.getSize("default_margin").width + height: childrenRect.height + spacing: 10 + + PrintCoreConfiguration + { + id: leftExtruderInfo + width: (parent.width-1) / 2 + printCoreConfiguration: printer.configuration[0] + } + + Rectangle + { + id: extruderSeperator + width: 1 + height: parent.height + color: lineColor + } + + PrintCoreConfiguration + { + id: rightExtruderInfo + width: (parent.width-1) / 2 + printCoreConfiguration: printer.configuration[1] + } + } + + Rectangle // Print progress + { + id: printProgressArea + anchors.right: parent.right + anchors.top: parent.top + height: showExtended ? parent.height: printProgressTitleBar.height + width: parent.width / 2 - UM.Theme.getSize("default_margin").width + border.width: UM.Theme.getSize("default_lining").width + border.color: lineColor + radius: cornerRadius + property var showExtended: { + if(printJob!= null) + { + var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup"]; + return extendStates.indexOf(printJob.status) !== -1; + } + return false + } + visible: + { + return true + } + + Item // Status and Percent + { + id: printProgressTitleBar + width: parent.width + //border.width: UM.Theme.getSize("default_lining").width + //border.color: lineColor + height: 40 + anchors.left: parent.left + + Label + { + id: statusText + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: parent.verticalCenter + anchors.right: progressText.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width + text: { + if(printJob != null) + { + if(printJob.status == "printing" || printJob.status == "post_print") + { + return catalog.i18nc("@label:status", "Printing") + } + else if(printJob.status == "wait_for_configuration") + { + return catalog.i18nc("@label:status", "Reserved") + } + else if(printJob.status == "wait_cleanup") + { + return catalog.i18nc("@label:status", "Finished") + } + else if (printJob.status == "pre_print" || printJob.status == "sent_to_printer") + { + return catalog.i18nc("@label:status", "Preparing") + } + else + { + return "" + } + } + return catalog.i18nc("@label:status", "Available") + } + + elide: Text.ElideRight + + font: UM.Theme.getFont("small") + } + Label + { + id: progressText + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + + anchors.top: statusText.top + + text: formatPrintJobPercent(printJob) + visible: printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.status) !== -1) + opacity: 0.65 + font: UM.Theme.getFont("very_small") + } + Rectangle + { + //TODO: This will become a progress bar in the future + width: parent.width + height: UM.Theme.getSize("default_lining").height + anchors.bottom: parent.bottom + anchors.left: parent.left + visible: printProgressArea.showExtended + color: lineColor + } + } + + Column + { + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + + anchors.top: printProgressTitleBar.bottom + anchors.topMargin: UM.Theme.getSize("default_margin").height + + width: parent.width - 2 * UM.Theme.getSize("default_margin").width + + visible: printJob != null && (["wait_cleanup", "printing", "pre_print", "wait_for_configuration"].indexOf(printJob.status) !== -1) + + Label // Status detail + { + text: + { + if(printJob != null) + { + if(printJob.status == "printing" ) + { + return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.time_total - printJob.time_elapsed) + } + if(printJob.status == "wait_cleanup") + { + return catalog.i18nc("@label", "Clear build plate") + } + if(printJob.status == "sent_to_printer" || printJob.status == "pre_print") + { + return catalog.i18nc("@label", "Preparing to print") + } + if(printJob.status == "wait_for_configuration") + { + return catalog.i18nc("@label", "Not accepting print jobs") + } + } + return "" + } + elide: Text.ElideRight + font: UM.Theme.getFont("default") + } + + Label // Status 2nd row + { + text: { + if(printJob != null) { + if(printJob.status == "printing" ) + { + return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed) + } + } + return ""; + } + + elide: Text.ElideRight + font: UM.Theme.getFont("default") + } + } + } + } + } +} diff --git a/plugins/UM3NetworkPrinting/PrinterTile.qml b/plugins/UM3NetworkPrinting/PrinterTile.qml new file mode 100644 index 0000000000..f240f3034f --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrinterTile.qml @@ -0,0 +1,54 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.3 as UM +import Cura 1.0 as Cura + +Rectangle +{ + id: base + width: 250 + height: 250 + signal clicked() + MouseArea + { + anchors.fill:parent + onClicked: base.clicked() + } + Rectangle + { + // TODO: Actually add UM icon / picture + width: 100 + height: 100 + border.width: UM.Theme.getSize("default_lining").width + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("default_margin").height + } + Label + { + id: nameLabel + anchors.bottom: ipLabel.top + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.rightMargin: UM.Theme.getSize("default_margin").width + text: modelData.friendly_name.toString() + font: UM.Theme.getFont("large") + elide: Text.ElideMiddle; + height: UM.Theme.getSize("section").height; + } + Label + { + id: ipLabel + text: modelData.ip_address.toString() + anchors.bottom: parent.bottom + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + font: UM.Theme.getFont("default") + height:10 + anchors.horizontalCenter: parent.horizontalCenter + } +} + diff --git a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml new file mode 100644 index 0000000000..4f138ee8d1 --- /dev/null +++ b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml @@ -0,0 +1,91 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.3 as UM + + +Item +{ + Rectangle + { + anchors.fill:parent + color: UM.Theme.getColor("viewport_overlay") + opacity: 0.5 + } + + MouseArea + { + anchors.fill: parent + onClicked: OutputDevice.selectAutomaticPrinter() + z: 0 + } + + Button + { + id: backButton + anchors.bottom: cameraImage.top + anchors.bottomMargin: UM.Theme.getSize("default_margin").width + anchors.right: cameraImage.right + + // TODO: Harcoded sizes + width: 20 + height: 20 + + onClicked: OutputDevice.selectAutomaticPrinter() + + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: control.width + height: control.height + sourceSize.width: width + sourceSize.height: width + source: UM.Theme.getIcon("cross1") + } + } + background: Item {} + } + } + + Image + { + id: cameraImage + width: Math.min(sourceSize.width === 0 ? 800 : sourceSize.width, maximumWidth) + height: (sourceSize.height === 0 ? 600 : sourceSize.height) * width / sourceSize.width + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + z: 1 + onVisibleChanged: + { + if(visible) + { + OutputDevice.startCamera() + } else + { + OutputDevice.stopCamera() + } + } + source: + { + if(OutputDevice.cameraImage) + { + return OutputDevice.cameraImage; + } + return ""; + } + } + + MouseArea + { + anchors.fill: cameraImage + onClicked: { /* no-op */ } + z: 1 + } + +} diff --git a/plugins/UM3NetworkPrinting/camera-icon.svg b/plugins/UM3NetworkPrinting/camera-icon.svg new file mode 100644 index 0000000000..2aafc4b6f4 --- /dev/null +++ b/plugins/UM3NetworkPrinting/camera-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/USBPrinting/FirmwareUpdateWindow.qml b/plugins/USBPrinting/FirmwareUpdateWindow.qml index f55aa4c56e..0e23348d60 100644 --- a/plugins/USBPrinting/FirmwareUpdateWindow.qml +++ b/plugins/USBPrinting/FirmwareUpdateWindow.qml @@ -11,10 +11,10 @@ UM.Dialog { id: base; - width: 500 * Screen.devicePixelRatio; - minimumWidth: 500 * Screen.devicePixelRatio; - height: 100 * Screen.devicePixelRatio; - minimumHeight: 100 * Screen.devicePixelRatio; + width: minimumWidth; + minimumWidth: 500 * screenScaleFactor; + height: minimumHeight; + minimumHeight: 100 * screenScaleFactor; visible: true; modality: Qt.ApplicationModal; diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index fba6678bff..06bc04b1fe 100755 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4094,7 +4094,7 @@ "raft_smoothing": { "label": "Raft Smoothing", - "description": "This setting control how much inner corners in the raft outline are rounded. Inward corners are rounded to a semi circle with a radius equal to the value given here. This setting also removes holes in the raft outline which are smaller than such a circle.", + "description": "This setting controls how much inner corners in the raft outline are rounded. Inward corners are rounded to a semi circle with a radius equal to the value given here. This setting also removes holes in the raft outline which are smaller than such a circle.", "unit": "mm", "type": "float", "default_value": 5, diff --git a/resources/qml/AboutDialog.qml b/resources/qml/AboutDialog.qml index 7df5709654..3a3ceb63d5 100644 --- a/resources/qml/AboutDialog.qml +++ b/resources/qml/AboutDialog.qml @@ -14,8 +14,8 @@ UM.Dialog //: About dialog title title: catalog.i18nc("@title:window","About Cura") - minimumWidth: 500 - minimumHeight: 650 + minimumWidth: 500 * screenScaleFactor + minimumHeight: 650 * screenScaleFactor width: minimumWidth height: minimumHeight diff --git a/resources/qml/AskOpenAsProjectOrModelsDialog.qml b/resources/qml/AskOpenAsProjectOrModelsDialog.qml index df451a58cf..7fb1b939f7 100644 --- a/resources/qml/AskOpenAsProjectOrModelsDialog.qml +++ b/resources/qml/AskOpenAsProjectOrModelsDialog.qml @@ -18,8 +18,8 @@ UM.Dialog id: base title: catalog.i18nc("@title:window", "Open project file") - width: 450 - height: 150 + width: 450 * screenScaleFactor + height: 150 * screenScaleFactor maximumHeight: height maximumWidth: width @@ -61,10 +61,10 @@ UM.Dialog Column { anchors.fill: parent - anchors.leftMargin: 20 - anchors.rightMargin: 20 - anchors.bottomMargin: 20 - spacing: 10 + anchors.leftMargin: 20 * screenScaleFactor + anchors.rightMargin: 20 * screenScaleFactor + anchors.bottomMargin: 20 * screenScaleFactor + spacing: 10 * screenScaleFactor Label { diff --git a/resources/qml/DiscardOrKeepProfileChangesDialog.qml b/resources/qml/DiscardOrKeepProfileChangesDialog.qml index 74b313a6b5..24a22227c1 100644 --- a/resources/qml/DiscardOrKeepProfileChangesDialog.qml +++ b/resources/qml/DiscardOrKeepProfileChangesDialog.qml @@ -14,8 +14,8 @@ UM.Dialog id: base title: catalog.i18nc("@title:window", "Discard or Keep changes") - width: 800 - height: 400 + width: 800 * screenScaleFactor + height: 400 * screenScaleFactor property var changesModel: Cura.UserChangesModel{ id: userChangesModel} onVisibilityChanged: { diff --git a/resources/qml/ExtruderButton.qml b/resources/qml/ExtruderButton.qml index 039158280a..88de797daf 100644 --- a/resources/qml/ExtruderButton.qml +++ b/resources/qml/ExtruderButton.qml @@ -67,7 +67,7 @@ Button height: UM.Theme.getSize("extruder_button_material").height radius: width / 2 - border.width: 1 + border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("extruder_button_material_border") opacity: !base.enabled ? 0.2 : 1.0 diff --git a/resources/qml/OpenFilesIncludingProjectsDialog.qml b/resources/qml/OpenFilesIncludingProjectsDialog.qml index 3c7275a2a4..ade9d0e820 100644 --- a/resources/qml/OpenFilesIncludingProjectsDialog.qml +++ b/resources/qml/OpenFilesIncludingProjectsDialog.qml @@ -17,8 +17,8 @@ UM.Dialog id: base title: catalog.i18nc("@title:window", "Open file(s)") - width: 420 - height: 170 + width: 420 * screenScaleFactor + height: 170 * screenScaleFactor maximumHeight: height maximumWidth: width @@ -28,7 +28,7 @@ UM.Dialog modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal; property var fileUrls: [] - property int spacerHeight: 10 + property int spacerHeight: 10 * screenScaleFactor function loadProjectFile(projectFile) { @@ -52,12 +52,12 @@ UM.Dialog Column { anchors.fill: parent - anchors.leftMargin: 20 - anchors.rightMargin: 20 - anchors.bottomMargin: 20 + anchors.leftMargin: 20 * screenScaleFactor + anchors.rightMargin: 20 * screenScaleFactor + anchors.bottomMargin: 20 * screenScaleFactor anchors.left: parent.left anchors.right: parent.right - spacing: 10 + spacing: 10 * screenScaleFactor Label { diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 452d5df794..6f9e84bfc3 100755 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -513,7 +513,7 @@ UM.PreferencesPage Column { - spacing: 4 + spacing: 4 * screenScaleFactor Label { @@ -523,7 +523,7 @@ UM.PreferencesPage ComboBox { id: choiceOnOpenProjectDropDownButton - width: 200 + width: 200 * screenScaleFactor model: ListModel { @@ -572,7 +572,7 @@ UM.PreferencesPage Column { - spacing: 4 + spacing: 4 * screenScaleFactor Label { @@ -583,7 +583,7 @@ UM.PreferencesPage ComboBox { id: choiceOnProfileOverrideDropDownButton - width: 200 + width: 200 * screenScaleFactor model: ListModel { diff --git a/resources/qml/Preferences/MachinesPage.qml b/resources/qml/Preferences/MachinesPage.qml index 4a62027f66..429400f170 100644 --- a/resources/qml/Preferences/MachinesPage.qml +++ b/resources/qml/Preferences/MachinesPage.qml @@ -91,7 +91,7 @@ UM.ManagementPage Item { - width: childrenRect.width + 2 + width: childrenRect.width + 2 * screenScaleFactor height: childrenRect.height Button { @@ -112,8 +112,6 @@ UM.ManagementPage { id: actionDialog property var content - minimumWidth: 350 - minimumHeight: 350 onContentChanged: { contents = content; @@ -257,8 +255,8 @@ UM.ManagementPage UM.RenameDialog { id: renameDialog; - width: 300 - height: 150 + width: 300 * screenScaleFactor + height: 150 * screenScaleFactor object: base.currentItem && base.currentItem.name ? base.currentItem.name : ""; property var machine_name_validator: Cura.MachineNameValidator { } validName: renameDialog.newName.match(renameDialog.machine_name_validator.machineNameRegex) != null; diff --git a/resources/qml/Settings/SettingCheckBox.qml b/resources/qml/Settings/SettingCheckBox.qml index 4ef7f59a77..9919c6aad9 100644 --- a/resources/qml/Settings/SettingCheckBox.qml +++ b/resources/qml/Settings/SettingCheckBox.qml @@ -119,8 +119,8 @@ SettingItem UM.RecolorImage { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - width: parent.width/2.5 - height: parent.height/2.5 + width: parent.width / 2.5 + height: parent.height / 2.5 sourceSize.width: width sourceSize.height: width color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text"); diff --git a/resources/qml/Settings/SettingComboBox.qml b/resources/qml/Settings/SettingComboBox.qml index c29ac123d6..2ff0eba2df 100644 --- a/resources/qml/Settings/SettingComboBox.qml +++ b/resources/qml/Settings/SettingComboBox.qml @@ -86,8 +86,8 @@ SettingItem source: UM.Theme.getIcon("arrow_bottom") width: UM.Theme.getSize("standard_arrow").width height: UM.Theme.getSize("standard_arrow").height - sourceSize.width: width + 5 - sourceSize.height: width + 5 + sourceSize.width: width + 5 * screenScaleFactor + sourceSize.height: width + 5 * screenScaleFactor color: UM.Theme.getColor("setting_control_text"); diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml index ac9f6a29eb..1c94ac938d 100644 --- a/resources/qml/Settings/SettingExtruder.qml +++ b/resources/qml/Settings/SettingExtruder.qml @@ -137,8 +137,8 @@ SettingItem source: UM.Theme.getIcon("arrow_bottom") width: UM.Theme.getSize("standard_arrow").width height: UM.Theme.getSize("standard_arrow").height - sourceSize.width: width + 5 - sourceSize.height: width + 5 + sourceSize.width: width + 5 * screenScaleFactor + sourceSize.height: width + 5 * screenScaleFactor color: UM.Theme.getColor("setting_control_text") } diff --git a/resources/qml/Settings/SettingOptionalExtruder.qml b/resources/qml/Settings/SettingOptionalExtruder.qml index 4e49f0440e..9d8ee72dd7 100644 --- a/resources/qml/Settings/SettingOptionalExtruder.qml +++ b/resources/qml/Settings/SettingOptionalExtruder.qml @@ -156,8 +156,8 @@ SettingItem source: UM.Theme.getIcon("arrow_bottom") width: UM.Theme.getSize("standard_arrow").width height: UM.Theme.getSize("standard_arrow").height - sourceSize.width: width + 5 - sourceSize.height: width + 5 + sourceSize.width: width + 5 * screenScaleFactor + sourceSize.height: width + 5 * screenScaleFactor color: UM.Theme.getColor("setting_control_text") } diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index 6d67b927bd..ef2f7c484c 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -213,7 +213,7 @@ Item { id: groovechildrect width: base.width * 0.55 - height: 2 + height: 2 * screenScaleFactor color: UM.Theme.getColor("quality_slider_unavailable") anchors.verticalCenter: qualitySlider.verticalCenter x: 0 @@ -229,8 +229,8 @@ Item { anchors.verticalCenter: parent.verticalCenter color: Cura.ProfilesModel.getItem(index).available ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") - width: 1 - height: 6 + width: 1 * screenScaleFactor + height: 6 * screenScaleFactor y: 0 x: qualityModel.qualitySliderStepWidth * index } @@ -260,18 +260,18 @@ Item { //Draw Available line groove: Rectangle { - implicitHeight: 2 + implicitHeight: 2 * screenScaleFactor color: UM.Theme.getColor("quality_slider_available") - radius: 1 + radius: 1 * screenScaleFactor } handle: Item { Rectangle { id: qualityhandleButton anchors.centerIn: parent color: control.enabled ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") - implicitWidth: 10 - implicitHeight: 10 - radius: 10 + implicitWidth: 10 * screenScaleFactor + implicitHeight: 10 * screenScaleFactor + radius: 10 * screenScaleFactor } } } @@ -309,7 +309,7 @@ Item text: catalog.i18nc("@label", "Slower") font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") + color: (qualityModel.availableTotalTicks > 0) ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") horizontalAlignment: Text.AlignLeft } @@ -320,7 +320,7 @@ Item text: catalog.i18nc("@label", "Faster") font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") + color: (qualityModel.availableTotalTicks > 0) ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") horizontalAlignment: Text.AlignRight } } @@ -370,7 +370,7 @@ Item //anchors.top: parent.top anchors.left: infillSlider.left - anchors.leftMargin: (infillSlider.value / infillSlider.stepSize) * (infillSlider.width / (infillSlider.maximumValue / infillSlider.stepSize)) - 10 + anchors.leftMargin: (infillSlider.value / infillSlider.stepSize) * (infillSlider.width / (infillSlider.maximumValue / infillSlider.stepSize)) - 10 * screenScaleFactor anchors.right: parent.right text: infillSlider.value + "%" @@ -411,8 +411,8 @@ Item { groove: Rectangle { id: groove - implicitWidth: 200 - implicitHeight: 2 + implicitWidth: 200 * screenScaleFactor + implicitHeight: 2 * screenScaleFactor color: control.enabled ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") radius: 1 } @@ -422,9 +422,9 @@ Item id: handleButton anchors.centerIn: parent color: control.enabled ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") - implicitWidth: 10 - implicitHeight: 10 - radius: 10 + implicitWidth: 10 * screenScaleFactor + implicitHeight: 10 * screenScaleFactor + radius: 10 * screenScaleFactor } } @@ -446,8 +446,8 @@ Item Rectangle { anchors.verticalCenter: parent.verticalCenter color: control.enabled ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable") - width: 1 - height: 6 + width: 1 * screenScaleFactor + height: 6 * screenScaleFactor y: 0 x: styleData.handleWidth / 2 + index * ((repeater.width - styleData.handleWidth) / (repeater.count-1)) visible: shouldShowTick(index) @@ -500,7 +500,7 @@ Item UM.RecolorImage { anchors.fill: parent - anchors.margins: 2 + anchors.margins: 2 * screenScaleFactor sourceSize.width: width sourceSize.height: width source: UM.Theme.getIcon(model.icon) @@ -516,7 +516,7 @@ Item property alias _hovered: enableGradualInfillMouseArea.containsMouse anchors.top: infillSlider.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height / 2 // closer to slider since it belongs to the same category anchors.left: infillCellRight.left style: UM.Theme.styles.checkbox @@ -609,7 +609,7 @@ Item visible: enableSupportCheckBox.visible anchors.top: infillCellRight.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height * 1.5 anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width anchors.verticalCenter: enableSupportCheckBox.verticalCenter @@ -624,8 +624,7 @@ Item id: enableSupportCheckBox property alias _hovered: enableSupportMouseArea.containsMouse - anchors.top: infillCellRight.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.top: enableSupportLabel.top anchors.left: infillCellRight.left style: UM.Theme.styles.checkbox; @@ -737,15 +736,19 @@ Item { id: adhesionHelperLabel visible: adhesionCheckBox.visible - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width - anchors.right: infillCellLeft.right - anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width - anchors.verticalCenter: adhesionCheckBox.verticalCenter - text: catalog.i18nc("@label", "Build Plate Adhesion"); - font: UM.Theme.getFont("default"); - color: UM.Theme.getColor("text"); + + text: catalog.i18nc("@label", "Build Plate Adhesion") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") elide: Text.ElideRight + + anchors { + left: parent.left + leftMargin: UM.Theme.getSize("sidebar_margin").width + right: infillCellLeft.right + rightMargin: UM.Theme.getSize("sidebar_margin").width + verticalCenter: adhesionCheckBox.verticalCenter + } } CheckBox diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index dd57fcc78b..2ab24f2023 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -67,7 +67,7 @@ Item } } - Item { height: UM.Theme.getSize("default_margin").height; width: 1; visible: extruders.count > 0 } + Item { height: UM.Theme.getSize("default_margin").height; width: UM.Theme.getSize("default_lining").width; visible: extruders.count > 0 } Repeater { diff --git a/resources/qml/WorkspaceSummaryDialog.qml b/resources/qml/WorkspaceSummaryDialog.qml index 5596d9c735..654bbaa3e8 100644 --- a/resources/qml/WorkspaceSummaryDialog.qml +++ b/resources/qml/WorkspaceSummaryDialog.qml @@ -13,10 +13,12 @@ UM.Dialog { title: catalog.i18nc("@title:window", "Save Project") - width: 500 - height: 400 + minimumWidth: 500 * screenScaleFactor + minimumHeight: 400 * screenScaleFactor + width: minimumWidth + height: minimumHeight - property int spacerHeight: 10 + property int spacerHeight: 10 * screenScaleFactor property bool dontShowAgain: true @@ -63,7 +65,7 @@ UM.Dialog Column { anchors.fill: parent - spacing: 2 + spacing: 2 * screenScaleFactor Label { id: titleLabel diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index be3e78990e..f2c387c6b7 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -309,7 +309,7 @@ QtObject { } Behavior on color { ColorAnimation { duration: 50; } } - border.width: (control.hasOwnProperty("needBorder") && control.needBorder) ? 2 : 0 + border.width: (control.hasOwnProperty("needBorder") && control.needBorder) ? 2 * screenScaleFactor : 0 border.color: Theme.getColor("tool_button_border") UM.RecolorImage { @@ -506,8 +506,8 @@ QtObject { source: control.iconSource; width: Theme.getSize("section_icon").width; height: Theme.getSize("section_icon").height; - sourceSize.width: width + 15 - sourceSize.height: width + 15 + sourceSize.width: width + 15 * screenScaleFactor + sourceSize.height: width + 15 * screenScaleFactor } } @@ -648,8 +648,8 @@ QtObject { source: Theme.getIcon("arrow_bottom") width: Theme.getSize("standard_arrow").width height: Theme.getSize("standard_arrow").height - sourceSize.width: width + 5 - sourceSize.height: width + 5 + sourceSize.width: width + 5 * screenScaleFactor + sourceSize.height: width + 5 * screenScaleFactor color: Theme.getColor("setting_control_text"); } @@ -707,8 +707,8 @@ QtObject { source: UM.Theme.getIcon("arrow_bottom") width: UM.Theme.getSize("standard_arrow").width height: UM.Theme.getSize("standard_arrow").height - sourceSize.width: width + 5 - sourceSize.height: width + 5 + sourceSize.width: width + 5 * screenScaleFactor + sourceSize.height: width + 5 * screenScaleFactor color: UM.Theme.getColor("setting_control_text") } @@ -734,8 +734,8 @@ QtObject { UM.RecolorImage { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - width: parent.width/2.5 - height: parent.height/2.5 + width: parent.width / 2.5 + height: parent.height / 2.5 sourceSize.width: width sourceSize.height: width color: Theme.getColor("checkbox_mark")