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")