mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-08 07:27:29 -06:00
Add in all of the changes for Cura Connect
CURA-4376
This commit is contained in:
parent
823807144f
commit
85efd9249c
13 changed files with 1769 additions and 42 deletions
243
plugins/UM3NetworkPrinting/ClusterControlItem.qml
Normal file
243
plugins/UM3NetworkPrinting/ClusterControlItem.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
plugins/UM3NetworkPrinting/ClusterMonitorItem.qml
Normal file
108
plugins/UM3NetworkPrinting/ClusterMonitorItem.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -272,6 +272,28 @@ Cura.MachineAction
|
||||||
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
|
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
|
Label
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
638
plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py
Normal file
638
plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py
Normal file
|
@ -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 <filename>{file_name}</filename> 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)
|
|
@ -17,7 +17,7 @@ import cura.Settings.ExtruderManager
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply
|
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply
|
||||||
from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot, QCoreApplication
|
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
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -102,7 +102,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._target_bed_temperature = 0
|
self._target_bed_temperature = 0
|
||||||
self._processing_preheat_requests = True
|
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.setName(key)
|
||||||
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
|
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
|
||||||
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
|
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
|
||||||
|
@ -340,6 +340,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||||
pass # It can happen that the wrapped c++ object is already deleted.
|
pass # It can happen that the wrapped c++ object is already deleted.
|
||||||
self._image_reply = None
|
self._image_reply = None
|
||||||
self._image_request = None
|
self._image_request = None
|
||||||
|
if self._use_stream:
|
||||||
|
# Reset image (To prevent old images from being displayed)
|
||||||
|
self._camera_image.fill(QColor(0, 0, 0))
|
||||||
|
self.newImage.emit()
|
||||||
|
|
||||||
def _startCamera(self):
|
def _startCamera(self):
|
||||||
if self._use_stream:
|
if self._use_stream:
|
||||||
|
@ -1007,7 +1011,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||||
reply_url = reply.url().toString()
|
reply_url = reply.url().toString()
|
||||||
|
|
||||||
if reply.operation() == QNetworkAccessManager.GetOperation:
|
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 status_code == 200:
|
||||||
if self._connection_state == ConnectionState.connecting:
|
if self._connection_state == ConnectionState.connecting:
|
||||||
self.setConnectionState(ConnectionState.connected)
|
self.setConnectionState(ConnectionState.connected)
|
||||||
|
@ -1025,7 +1030,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code)
|
Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code)
|
||||||
pass # TODO: Handle errors
|
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:
|
if status_code == 200:
|
||||||
try:
|
try:
|
||||||
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||||
|
|
|
@ -1,26 +1,31 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the AGPLv3 or higher.
|
||||||
|
|
||||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
import os
|
||||||
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 time
|
import time
|
||||||
import json
|
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.
|
## 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.
|
# 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.
|
# If we discover a printer that has the same key as the active machine instance a connection is made.
|
||||||
@signalemitter
|
@signalemitter
|
||||||
class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._zero_conf = None
|
self._zero_conf = None
|
||||||
|
@ -29,6 +34,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
|
|
||||||
self._api_version = "1"
|
self._api_version = "1"
|
||||||
self._api_prefix = "/api/v" + self._api_version + "/"
|
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 = QNetworkAccessManager()
|
||||||
self._network_manager.finished.connect(self._onNetworkRequestFinished)
|
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._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._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
|
||||||
|
|
||||||
|
self._network_requests_buffer = {} # store api responses until data is complete
|
||||||
|
|
||||||
addPrinterSignal = Signal()
|
addPrinterSignal = Signal()
|
||||||
removePrinterSignal = Signal()
|
removePrinterSignal = Signal()
|
||||||
printerListChanged = Signal()
|
printerListChanged = Signal()
|
||||||
|
@ -91,6 +100,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
self.addPrinter(instance_name, address, properties)
|
self.addPrinter(instance_name, address, properties)
|
||||||
|
|
||||||
self.checkManualPrinter(address)
|
self.checkManualPrinter(address)
|
||||||
|
self.checkClusterPrinter(address)
|
||||||
|
|
||||||
def removeManualPrinter(self, key, address = None):
|
def removeManualPrinter(self, key, address = None):
|
||||||
if key in self._printers:
|
if key in self._printers:
|
||||||
|
@ -105,18 +115,26 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
def checkManualPrinter(self, address):
|
def checkManualPrinter(self, address):
|
||||||
# Check if a printer exists at this address
|
# Check if a printer exists at this address
|
||||||
# If a printer responds, it will replace the preliminary printer created above
|
# 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)
|
name_request = QNetworkRequest(url)
|
||||||
self._network_manager.get(name_request)
|
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.
|
## Handler for all requests that have finished.
|
||||||
def _onNetworkRequestFinished(self, reply):
|
def _onNetworkRequestFinished(self, reply):
|
||||||
reply_url = reply.url().toString()
|
reply_url = reply.url().toString()
|
||||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||||
|
|
||||||
if reply.operation() == QNetworkAccessManager.GetOperation:
|
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:
|
if status_code == 200:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
|
@ -125,28 +143,51 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
Logger.log("e", "Printer returned incorrect UTF-8.")
|
Logger.log("e", "Printer returned incorrect UTF-8.")
|
||||||
return
|
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 = {
|
if address not in self._network_requests_buffer:
|
||||||
b"name": system_info["name"].encode("utf-8"),
|
self._network_requests_buffer[address] = {}
|
||||||
b"address": address.encode("utf-8"),
|
self._network_requests_buffer[address]["system"] = system_info
|
||||||
b"firmware_version": system_info["firmware"].encode("utf-8"),
|
elif "origin=check_cluster" in reply_url:
|
||||||
b"manual": b"true",
|
if address not in self._network_requests_buffer:
|
||||||
b"machine": machine.encode("utf-8")
|
self._network_requests_buffer[address] = {}
|
||||||
}
|
if status_code == 200:
|
||||||
if instance_name in self._printers:
|
# We know it's a cluster printer
|
||||||
# Only replace the printer if it is still in the list of (manual) printers
|
Logger.log("d", "Cluster printer detected: [%s]", reply.url())
|
||||||
self.removePrinter(instance_name)
|
self._network_requests_buffer[address]["cluster"] = True
|
||||||
self.addPrinter(instance_name, address, properties)
|
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.
|
## Stop looking for devices on network.
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
@ -175,8 +216,13 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
|
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.
|
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||||
def addPrinter(self, name, address, properties):
|
def addPrinter(self, name, address, properties, force_cluster=False):
|
||||||
printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
|
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
|
self._printers[printer.getKey()] = printer
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
|
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||||
|
@ -238,3 +284,21 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
elif state_change == ServiceStateChange.Removed:
|
elif state_change == ServiceStateChange.Removed:
|
||||||
Logger.log("d", "Bonjour service removed: %s" % name)
|
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||||
self.removePrinterSignal.emit(str(name))
|
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()))
|
||||||
|
|
18
plugins/UM3NetworkPrinting/OpenPanelButton.qml
Normal file
18
plugins/UM3NetworkPrinting/OpenPanelButton.qml
Normal file
|
@ -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
|
||||||
|
}
|
33
plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml
Normal file
33
plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
103
plugins/UM3NetworkPrinting/PrintWindow.qml
Normal file
103
plugins/UM3NetworkPrinting/PrintWindow.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
345
plugins/UM3NetworkPrinting/PrinterInfoBlock.qml
Normal file
345
plugins/UM3NetworkPrinting/PrinterInfoBlock.qml
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
plugins/UM3NetworkPrinting/PrinterTile.qml
Normal file
54
plugins/UM3NetworkPrinting/PrinterTile.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
91
plugins/UM3NetworkPrinting/PrinterVideoStream.qml
Normal file
91
plugins/UM3NetworkPrinting/PrinterVideoStream.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3
plugins/UM3NetworkPrinting/camera-icon.svg
Normal file
3
plugins/UM3NetworkPrinting/camera-icon.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="14" viewBox="0 0 21 14">
|
||||||
|
<path fill="none" fill-rule="evenodd" stroke="#464B4E" stroke-width="1.5" d="M19.295 2.83L16.25 4.31V2c0-.69-.56-1.25-1.25-1.25H2C1.31.75.75 1.31.75 2v10c0 .69.56 1.25 1.25 1.25h13c.69 0 1.25-.56 1.25-1.25V9.69l3.045 1.48a.85.85 0 0 0 .367.08c.355 0 .584-.181.584-.31V3.06c0-.026-.011-.058-.04-.096-.16-.206-.592-.289-.911-.134z" opacity=".85"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 441 B |
Loading…
Add table
Add a link
Reference in a new issue