diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index f690456913..cc38149c03 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -115,6 +115,7 @@ from . import CuraActions from . import PlatformPhysics from . import PrintJobPreviewImageProvider from .AutoSave import AutoSave +from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel from .Machines.Models.MachineListModel import MachineListModel from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel from .Machines.Models.IntentSelectionModel import IntentSelectionModel @@ -1191,6 +1192,7 @@ class CuraApplication(QtApplication): qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel") qmlRegisterType(MachineListModel, "Cura", 1, 0, "MachineListModel") + qmlRegisterType(CompatibleMachineModel, "Cura", 1, 0, "CompatibleMachineModel") self.processEvents() qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel") diff --git a/cura/Machines/Models/CompatibleMachineModel.py b/cura/Machines/Models/CompatibleMachineModel.py new file mode 100644 index 0000000000..029567cdec --- /dev/null +++ b/cura/Machines/Models/CompatibleMachineModel.py @@ -0,0 +1,76 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional + +from PyQt6.QtCore import Qt, QObject, pyqtSlot, pyqtProperty, pyqtSignal + +from UM.Logger import Logger +from UM.Qt.ListModel import ListModel +from UM.i18n import i18nCatalog + + +class CompatibleMachineModel(ListModel): + NameRole = Qt.ItemDataRole.UserRole + 1 + UniqueIdRole = Qt.ItemDataRole.UserRole + 2 + ExtrudersRole = Qt.ItemDataRole.UserRole + 3 + + def __init__(self, parent: Optional[QObject] = None) -> None: + super().__init__(parent) + + self._catalog = i18nCatalog("cura") + + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.UniqueIdRole, "unique_id") + self.addRoleName(self.ExtrudersRole, "extruders") + + self._update() + + from cura.CuraApplication import CuraApplication + machine_manager = CuraApplication.getInstance().getMachineManager() + machine_manager.globalContainerChanged.connect(self._update) + machine_manager.outputDevicesChanged.connect(self._update) + + def _update(self) -> None: + self.clear() + + from cura.CuraApplication import CuraApplication + machine_manager = CuraApplication.getInstance().getMachineManager() + + # Loop over the output-devices, not the stacks; need all applicable configurations, not just the current loaded one. + for output_device in machine_manager.printerOutputDevices: + for printer in output_device.printers: + extruder_configs = dict() + + # initialize & add current active material: + for extruder in printer.extruders: + materials = [{ + "brand": extruder.activeMaterial.brand, + "name": extruder.activeMaterial.name, + "hexcolor": extruder.activeMaterial.color, + }] + extruder_configs[extruder.getPosition()] = { + "position": extruder.getPosition(), + "core": extruder.hotendID, + "materials": materials + } + + # add currently inactive, but possible materials: + for configuration in printer.availableConfigurations: + for extruder in configuration.extruderConfigurations: + if not extruder.position in extruder_configs: + Logger.log("w", f"No active extruder for position {extruder.position}.") + continue + + extruder_configs[extruder.position]["materials"].append({ + "brand": extruder.material.brand, + "name": extruder.material.name, + "hexcolor": extruder.material.color + }) + + if any([len(extruder["materials"]) > 0 for extruder in extruder_configs.values()]): + self.appendItem({ + "name": printer.name, + "unique_id": printer.name, # <- Can assume the cloud doesn't have duplicate names? + "extruders": list(extruder_configs.values()) + }) diff --git a/cura/Machines/Models/MachineListModel.py b/cura/Machines/Models/MachineListModel.py index 919d593200..4db1082863 100644 --- a/cura/Machines/Models/MachineListModel.py +++ b/cura/Machines/Models/MachineListModel.py @@ -5,10 +5,13 @@ # online cloud connected printers are represented within this ListModel. Additional information such as the number of # connected printers for each printer type is gathered. -from PyQt6.QtCore import Qt, QTimer, pyqtSlot, pyqtProperty, pyqtSignal +from typing import Optional + +from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal from UM.Qt.ListModel import ListModel from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.Interfaces import ContainerInterface from UM.i18n import i18nCatalog from UM.Util import parseBool from cura.PrinterOutput.PrinterOutputDevice import ConnectionType @@ -27,7 +30,7 @@ class MachineListModel(ListModel): IsAbstractMachineRole = Qt.ItemDataRole.UserRole + 7 ComponentTypeRole = Qt.ItemDataRole.UserRole + 8 - def __init__(self, parent=None) -> None: + def __init__(self, parent: Optional[QObject] = None) -> None: super().__init__(parent) self._show_cloud_printers = False @@ -66,7 +69,7 @@ class MachineListModel(ListModel): self._updateDelayed() self.showCloudPrintersChanged.emit(show_cloud_printers) - def _onContainerChanged(self, container) -> None: + def _onContainerChanged(self, container: ContainerInterface) -> None: """Handler for container added/removed events from registry""" # We only need to update when the added / removed container GlobalStack @@ -79,14 +82,15 @@ class MachineListModel(ListModel): def _update(self) -> None: self.clear() + from cura.CuraApplication import CuraApplication + machines_manager = CuraApplication.getInstance().getMachineManager() + other_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type="machine") abstract_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(is_abstract_machine = "True") abstract_machine_stacks.sort(key = lambda machine: machine.getName(), reverse = True) for abstract_machine in abstract_machine_stacks: definition_id = abstract_machine.definition.getId() - from cura.CuraApplication import CuraApplication - machines_manager = CuraApplication.getInstance().getMachineManager() online_machine_stacks = machines_manager.getMachinesWithDefinition(definition_id, online_only = True) online_machine_stacks = list(filter(lambda machine: machine.hasNetworkedConnection(), online_machine_stacks)) @@ -128,11 +132,11 @@ class MachineListModel(ListModel): return self.appendItem({ - "componentType": "MACHINE", - "name": container_stack.getName(), + "componentType": "MACHINE", + "name": container_stack.getName(), "id": container_stack.getId(), "metadata": container_stack.getMetaData().copy(), "isOnline": is_online, "isAbstractMachine": parseBool(container_stack.getMetaDataEntry("is_abstract_machine", False)), "machineCount": machine_count, - }) + }) diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index 4cc3e81f56..d54092b8c9 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -13,9 +13,9 @@ class ExtruderConfigurationModel(QObject): def __init__(self, position: int = -1) -> None: super().__init__() - self._position = position # type: int - self._material = None # type: Optional[MaterialOutputModel] - self._hotend_id = None # type: Optional[str] + self._position: int = position + self._material: Optional[MaterialOutputModel] = None + self._hotend_id: Optional[str] = None def setPosition(self, position: int) -> None: self._position = position diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index ca45cdc4b9..2051ce1b99 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -99,7 +99,7 @@ class MachineManager(QObject): self._application.getPreferences().addPreference("cura/active_machine", "") - self._printer_output_devices = [] # type: List[PrinterOutputDevice] + self._printer_output_devices: List[PrinterOutputDevice] = [] self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) # There might already be some output devices by the time the signal is connected self._onOutputDevicesChanged() @@ -112,7 +112,7 @@ class MachineManager(QObject): self._application.callLater(self.setInitialActiveMachine) - containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer] + containers: List[InstanceContainer] = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) if containers: containers[0].nameChanged.connect(self._onMaterialNameChanged) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py index 8448c095c8..4ee74550a4 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py @@ -1,15 +1,19 @@ from time import time -from typing import List +from typing import Callable, List, Optional -from PyQt6.QtCore import QObject +from PyQt6.QtCore import QObject, pyqtSlot from PyQt6.QtNetwork import QNetworkReply from UM import i18nCatalog from UM.Logger import Logger +from UM.FileHandler.FileHandler import FileHandler +from UM.Resources import Resources +from UM.Scene.SceneNode import SceneNode + +from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from .CloudApiClient import CloudApiClient -from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterWithConfigResponse import CloudClusterWithConfigResponse from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice @@ -19,7 +23,7 @@ I18N_CATALOG = i18nCatalog("cura") class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): API_CHECK_INTERVAL = 10.0 # seconds - def __init__(self, api_client: CloudApiClient, printer_type: str, parent: QObject = None) -> None: + def __init__(self, api_client: CloudApiClient, printer_type: str, request_write_callback: Callable, refresh_callback: Callable, parent: QObject = None) -> None: self._api = api_client properties = {b"printer_type": printer_type.encode()} @@ -31,6 +35,11 @@ class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): parent=parent ) + self._on_print_dialog: Optional[QObject] = None + self._nodes: List[SceneNode] = None + self._request_write_callback = request_write_callback + self._refresh_callback = refresh_callback + self._setInterfaceElements() def connect(self) -> None: @@ -41,7 +50,6 @@ class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): Logger.log("i", "Attempting to connect AbstractCloudOutputDevice %s", self.key) super().connect() - #CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange) self._update() def disconnect(self) -> None: @@ -84,4 +92,31 @@ class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._updatePrinters(all_configurations) def _onError(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: - pass + Logger.log("w", f"Failed to get clusters by machine type: {str(error)}.") + + @pyqtSlot(str) + def printerSelected(self, unique_id: str): + self._request_write_callback(unique_id, self._nodes) + if self._on_print_dialog: + self._on_print_dialog.close() + + @pyqtSlot() + def refresh(self): + self._refresh_callback() + self._update() + + def _openChoosePrinterDialog(self) -> None: + if self._on_print_dialog is None: + qml_path = Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Dialogs", "ChoosePrinterDialog.qml") + self._on_print_dialog = CuraApplication.getInstance().createQmlComponent(qml_path, {}) + if self._on_print_dialog is None: # Failed to load QML file. + return + self._on_print_dialog.setProperty("manager", self) + self._on_print_dialog.show() + + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs) -> None: + if not nodes or len(nodes) < 1: + Logger.log("w", "Nothing to print.") + return + self._nodes = nodes + self._openChoosePrinterDialog() diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index bdae34a860..abfe863749 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -172,6 +172,13 @@ class CloudOutputDeviceManager: self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) + def _requestWrite(self, unique_id: str, nodes: List["SceneNode"]): + for remote in self._remote_clusters.values(): + if unique_id == remote.name: # No other id-type would match. Assume cloud doesn't have duplicate names. + remote.requestWrite(nodes) + return + Logger.log("e", f"Failed writing to specific cloud printer: {unique_id} not in remote clusters.") + def _createMachineStacksForDiscoveredClusters(self, discovered_clusters: List[CloudClusterResponse]) -> None: """**Synchronously** create machines for discovered devices @@ -193,7 +200,7 @@ class CloudOutputDeviceManager: output_device = CloudOutputDevice(self._api, cluster_data) if cluster_data.printer_type not in self._abstract_clusters: - self._abstract_clusters[cluster_data.printer_type] = AbstractCloudOutputDevice(self._api, cluster_data.printer_type) + self._abstract_clusters[cluster_data.printer_type] = AbstractCloudOutputDevice(self._api, cluster_data.printer_type, self._requestWrite, self.refreshConnections) # Ensure that the abstract machine is added (either because it was never added, or it somehow got # removed) _abstract_machine = CuraStackBuilder.createAbstractMachine(cluster_data.printer_type) diff --git a/resources/qml/Dialogs/ChoosePrinterDialog.qml b/resources/qml/Dialogs/ChoosePrinterDialog.qml new file mode 100644 index 0000000000..b7079fcabd --- /dev/null +++ b/resources/qml/Dialogs/ChoosePrinterDialog.qml @@ -0,0 +1,94 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 2.9 +import QtQuick.Layouts 2.10 + +import UM 1.5 as UM +import Cura 1.0 as Cura + +UM.Dialog +{ + property var manager + + id: base + + title: catalog.i18nc("@title:window", "Select Printer") + + backgroundColor: UM.Theme.getColor("background_2") + + width: minimumWidth + minimumWidth: 550 * screenScaleFactor + height: minimumHeight + minimumHeight: 550 * screenScaleFactor + + modality: Qt.ApplicationModal + + ScrollView + { + // Workaround for Windowing bugs in Qt: + width: 550 * screenScaleFactor - 3 * UM.Theme.getSize("default_margin").width + height: 550 * screenScaleFactor - 3 * UM.Theme.getSize("default_margin").height + + UM.I18nCatalog + { + id: catalog + name: "cura" + } + + anchors.fill: parent + Column + { + anchors.fill: parent + spacing: UM.Theme.getSize("default_margin").height + + Item + { + width: parent.width + height: childrenRect.height + + UM.Label + { + anchors.left: parent.left + text: catalog.i18nc("@title:label", "Compatible Printers") + font: UM.Theme.getFont("large") + } + + UM.SimpleButton + { + anchors.right: parent.right + + width: UM.Theme.getSize("small_button").width + height: UM.Theme.getSize("small_button").height + iconSource: UM.Theme.getIcon("ArrowDoubleCircleRight") + color: UM.Theme.getColor("text_link") + hoverColor: UM.Theme.getColor("text_scene_hover") + + onClicked: manager.refresh() + } + } + + Repeater + { + id: contents + + model: Cura.CompatibleMachineModel {} + + delegate: Cura.PrintSelectorCard + { + name: model.name + unique_id: model.unique_id + extruders: model.extruders + manager: base.manager + } + } + + UM.Label + { + visible: contents.count < 1 + text: catalog.i18nc("@description", "No compatible printers, that are currently online, where found.") + } + } + } +} diff --git a/resources/qml/ExtruderIcon.qml b/resources/qml/ExtruderIcon.qml index fb3269ca78..718f1bcd87 100644 --- a/resources/qml/ExtruderIcon.qml +++ b/resources/qml/ExtruderIcon.qml @@ -8,16 +8,16 @@ Item { id: extruderIconItem - implicitWidth: UM.Theme.getSize("extruder_icon").width - implicitHeight: UM.Theme.getSize("extruder_icon").height - property bool checked: true property color materialColor property alias textColor: extruderNumberText.color property bool extruderEnabled: true - property var iconSize + property var iconSize: UM.Theme.getSize("extruder_icon").width property string iconVariant: "medium" + implicitWidth: iconSize + implicitHeight: iconSize + Item { opacity: extruderEnabled ? 1 : UM.Theme.getColor("extruder_disabled").a @@ -27,8 +27,8 @@ Item UM.ColorImage { anchors.fill: parent - width: mainIcon.width - height: mainIcon.height + width: iconSize + height: iconSize source: UM.Theme.getIcon("ExtruderColor", iconVariant) color: materialColor @@ -37,8 +37,8 @@ Item { id: mainIcon anchors.fill: parent - width: UM.Theme.getSize("extruder_icon").width - height: UM.Theme.getSize("extruder_icon").height + width: iconSize + height: iconSize source: UM.Theme.getIcon("Extruder", iconVariant) color: extruderNumberText.color diff --git a/resources/qml/PrinterSelector/PrintSelectorCard.qml b/resources/qml/PrinterSelector/PrintSelectorCard.qml new file mode 100644 index 0000000000..517a0e164a --- /dev/null +++ b/resources/qml/PrinterSelector/PrintSelectorCard.qml @@ -0,0 +1,143 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 2.9 +import QtQuick.Layouts 2.10 + +import UM 1.5 as UM +import Cura 1.0 as Cura + +Rectangle +{ + property alias name: printerTitle.text + property string unique_id + property var extruders + property var manager + + width: parent.width + height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height + + color: UM.Theme.getColor("background_1") + border.color: UM.Theme.getColor("border_main") + border.width: UM.Theme.getSize("default_lining").width + + RowLayout + { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: UM.Theme.getSize("default_margin").width + + Cura.IconWithText + { + id: printerTitle + + Layout.preferredWidth: parent.width / 3 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + Layout.fillHeight: false + + source: UM.Theme.getIcon("Printer") + spacing: UM.Theme.getSize("thin_margin").width + iconSize: UM.Theme.getSize("medium_button_icon").width + font: UM.Theme.getFont("medium_bold") + } + + ColumnLayout + { + id: extruderInformation + Layout.fillWidth: true + Layout.preferredWidth: parent.width / 2 + Layout.alignment: Qt.AlignTop + spacing: UM.Theme.getSize("default_margin").width + + Repeater + { + model: extruders + + Item + { + height: childrenRect.height + + Cura.ExtruderIcon + { + id: extruderIcon + anchors.top: parent.top + anchors.left: parent.left + materialColor: modelData.materials.length == 1 ? modelData.materials[0].hexcolor : "white" + iconSize: UM.Theme.getSize("medium_button_icon").width + } + + UM.Label + { + id: extruderCore + anchors.verticalCenter: extruderIcon.verticalCenter + anchors.left: extruderIcon.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + text: modelData.core + font: UM.Theme.getFont("default_bold") + } + + UM.Label + { + id: singleMaterialText + anchors.left: extruderCore.right + anchors.verticalCenter: extruderCore.verticalCenter + anchors.leftMargin: UM.Theme.getSize("default_margin").width + text: modelData.materials.length == 1 ? `${modelData.materials[0].brand} ${modelData.materials[0].name}` : "" + visible: modelData.materials.length == 1 + } + + ColumnLayout + { + id: multiMaterialText + anchors.top: extruderCore.bottom + anchors.left: extruderCore.left + anchors.topMargin: UM.Theme.getSize("narrow_margin").height + visible: modelData.materials.length > 1 + Repeater + { + model: modelData.materials + UM.Label + { + text: `${modelData.brand} ${modelData.name}` + } + } + } + } + } + } + + Button + { + id: printButton + + implicitWidth: UM.Theme.getSize("medium_button").width + implicitHeight: implicitWidth + Layout.alignment: Qt.AlignTop + padding: 0 + + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("border_accent_1") + color: printButton.hovered ? UM.Theme.getColor("toolbar_button_hover"): UM.Theme.getColor("background_1") + } + + contentItem: Item + { + UM.ColorImage + { + anchors.centerIn: parent + source: UM.Theme.getIcon("Printer") + color: UM.Theme.getColor("border_accent_1") + width: UM.Theme.getSize("small_button_icon").width + height: width + } + } + + onClicked: manager.printerSelected(unique_id) + } + } +} diff --git a/resources/qml/qmldir b/resources/qml/qmldir index 6ec3ca91c8..d5184f1d8c 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -17,6 +17,7 @@ PrinterTypeLabel 1.0 PrinterTypeLabel.qml ViewsSelector 1.0 ViewsSelector.qml SettingView 1.0 SettingView.qml ProfileMenu 1.0 ProfileMenu.qml +PrintSelectorCard 1.0 PrintSelectorCard.qml # Cura/WelcomePages