diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index f701c94797..3cd0ecbf97 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.MachineListModel import MachineListModel from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel from .Machines.Models.IntentSelectionModel import IntentSelectionModel from .SingleInstance import SingleInstance @@ -1194,6 +1195,7 @@ class CuraApplication(QtApplication): qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel") + qmlRegisterType(MachineListModel, "Cura", 1, 0, "MachineListModel") self.processEvents() qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel") diff --git a/cura/Machines/Models/MachineListModel.py b/cura/Machines/Models/MachineListModel.py new file mode 100644 index 0000000000..f3781cfd60 --- /dev/null +++ b/cura/Machines/Models/MachineListModel.py @@ -0,0 +1,92 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt6.QtCore import Qt, QTimer + +from UM.Qt.ListModel import ListModel +from UM.Settings.ContainerStack import ContainerStack +from UM.i18n import i18nCatalog +from UM.Util import parseBool + +from cura.Settings.AbstractMachine import AbstractMachine +from cura.Settings.CuraContainerRegistry import CuraContainerRegistry +from cura.Settings.GlobalStack import GlobalStack + + +class MachineListModel(ListModel): + NameRole = Qt.ItemDataRole.UserRole + 1 + IdRole = Qt.ItemDataRole.UserRole + 2 + HasRemoteConnectionRole = Qt.ItemDataRole.UserRole + 3 + MetaDataRole = Qt.ItemDataRole.UserRole + 4 + IsOnlineRole = Qt.ItemDataRole.UserRole + 5 + MachineTypeRole = Qt.ItemDataRole.UserRole + 6 + MachineCountRole = Qt.ItemDataRole.UserRole + 7 + + def __init__(self, parent=None) -> None: + super().__init__(parent) + + self._catalog = i18nCatalog("cura") + + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.IdRole, "id") + self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection") + self.addRoleName(self.MetaDataRole, "metadata") + self.addRoleName(self.IsOnlineRole, "isOnline") + self.addRoleName(self.MachineTypeRole, "machineType") + self.addRoleName(self.MachineCountRole, "machineCount") + + self._change_timer = QTimer() + self._change_timer.setInterval(200) + self._change_timer.setSingleShot(True) + self._change_timer.timeout.connect(self._update) + + # Listen to changes + CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged) + CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged) + CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) + self._updateDelayed() + + def _onContainerChanged(self, container) -> None: + """Handler for container added/removed events from registry""" + + # We only need to update when the added / removed container GlobalStack + if isinstance(container, GlobalStack): + self._updateDelayed() + + def _updateDelayed(self) -> None: + self._change_timer.start() + + def _update(self) -> None: + self.setItems([]) # Clear items + + other_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type="machine") + + abstract_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "abstract_machine") + abstract_machine_stacks.sort(key = lambda machine: machine.getName(), reverse = True) + + for abstract_machine in abstract_machine_stacks: + online_machine_stacks = AbstractMachine.getMachines(abstract_machine, online_only = True) + + # Create a list item for abstract machine + self.addItem(abstract_machine, len(online_machine_stacks)) + + # Create list of machines that are children of the abstract machine + for stack in online_machine_stacks: + self.addItem(stack) + # Remove this machine from the other stack list + other_machine_stacks.remove(stack) + + for stack in other_machine_stacks: + self.addItem(stack) + + def addItem(self, container_stack: ContainerStack, machine_count: int = 0) -> None: + if parseBool(container_stack.getMetaDataEntry("hidden", False)): + return + + self.appendItem({"name": container_stack.getName(), + "id": container_stack.getId(), + "metadata": container_stack.getMetaData().copy(), + "isOnline": parseBool(container_stack.getMetaDataEntry("is_online", False)), + "machineType": container_stack.getMetaDataEntry("type"), + "machineCount": machine_count, + }) diff --git a/cura/Settings/AbstractMachine.py b/cura/Settings/AbstractMachine.py index a89201a294..86909b6e29 100644 --- a/cura/Settings/AbstractMachine.py +++ b/cura/Settings/AbstractMachine.py @@ -1,6 +1,7 @@ from typing import List from UM.Settings.ContainerStack import ContainerStack +from UM.Util import parseBool from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from cura.Settings.GlobalStack import GlobalStack from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase @@ -14,14 +15,30 @@ class AbstractMachine(GlobalStack): super().__init__(container_id) self.setMetaDataEntry("type", "abstract_machine") - def getMachines(self) -> List[ContainerStack]: - from cura.CuraApplication import CuraApplication + @classmethod + def getMachines(cls, abstract_machine: ContainerStack, online_only = False) -> List[ContainerStack]: + """ Fetches all container stacks that match definition_id with an abstract machine. + :param abstractMachine: The abstract machine stack. + :return: A list of Containers or an empty list if abstract_machine is not an "abstract_machine" + """ + if not abstract_machine.getMetaDataEntry("type") == "abstract_machine": + return [] + + from cura.CuraApplication import CuraApplication # In function to avoid circular import application = CuraApplication.getInstance() registry = application.getContainerRegistry() - printer_type = self.definition.getId() - return [machine for machine in registry.findContainerStacks(type="machine") if machine.definition.id == printer_type and ConnectionType.CloudConnection in machine.configuredConnectionTypes] + machines = registry.findContainerStacks(type="machine") + # Filter machines that match definition + machines = filter(lambda machine: machine.definition.id == abstract_machine.definition.getId(), machines) + # Filter only LAN and Cloud printers + machines = filter(lambda machine: ConnectionType.CloudConnection in machine.configuredConnectionTypes or ConnectionType.NetworkConnection in machine.configuredConnectionTypes, machines) + if online_only: + # LAN printers have is_online = False but should still be included + machines = filter(lambda machine: parseBool(machine.getMetaDataEntry("is_online", False) or ConnectionType.NetworkConnection in machine.configuredConnectionTypes), machines) + + return list(machines) ## private: diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 7eff275457..d711a61243 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -297,6 +297,7 @@ class CuraStackBuilder: name = machine_definition.getName() stack = AbstractMachine(abstract_machine_id) + stack.setMetaDataEntry("is_online", True) stack.setDefinition(machine_definition) cls.createUserContainer( name, diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 0cd5304cf9..bddd383b23 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -232,6 +232,9 @@ class LocalClusterOutputDeviceManager: self._connectToOutputDevice(device, new_machine) self._showCloudFlowMessage(device) + _abstract_machine = CuraStackBuilder.createAbstractMachine(device.printerType) + + def _storeManualAddress(self, address: str) -> None: """Add an address to the stored preferences.""" diff --git a/resources/qml/PrinterSelector/MachineListButton.qml b/resources/qml/PrinterSelector/MachineListButton.qml new file mode 100644 index 0000000000..4511c72b4c --- /dev/null +++ b/resources/qml/PrinterSelector/MachineListButton.qml @@ -0,0 +1,87 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Controls 2.3 + +import UM 1.5 as UM +import Cura 1.0 as Cura + + +Button +{ + id: machineListButton + + width: parent.width + height: UM.Theme.getSize("large_button").height + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("default_margin").width + checkable: true + hoverEnabled: true + + contentItem: Item + { + width: machineListButton.width - machineListButton.leftPadding - machineListButton.rightPadding + height: UM.Theme.getSize("action_button").height + + UM.ColorImage + { + id: printerIcon + height: UM.Theme.getSize("medium_button").height + width: UM.Theme.getSize("medium_button").width + color: UM.Theme.getColor("machine_selector_printer_icon") + visible: model.machineType == "abstract_machine" || !model.isOnline + source: model.machineType == "abstract_machine" ? UM.Theme.getIcon("PrinterTriple", "medium") : UM.Theme.getIcon("Printer", "medium") + + anchors + { + left: parent.left + verticalCenter: parent.verticalCenter + } + } + + UM.Label + { + id: buttonText + anchors + { + left: printerIcon.right + right: printerCount.left + verticalCenter: parent.verticalCenter + leftMargin: UM.Theme.getSize("default_margin").width + } + text: machineListButton.text + font: model.machineType == "abstract_machine" ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium") + visible: text != "" + elide: Text.ElideRight + } + + Rectangle + { + id: printerCount + color: UM.Theme.getColor("background_2") + radius: height + width: height + anchors + { + right: parent.right + top: buttonText.top + bottom: buttonText.bottom + } + visible: model.machineType == "abstract_machine" + + UM.Label + { + text: model.machineCount + anchors.centerIn: parent + font: UM.Theme.getFont("default_bold") + } + } + } + + background: Rectangle + { + id: backgroundRect + color: machineListButton.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent" + } +} diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 77cd2be409..869d536a00 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -192,7 +192,7 @@ Cura.ExpandablePopup contentItem: Item { id: popup - implicitWidth: UM.Theme.getSize("machine_selector_widget_content").width + implicitWidth: Math.max(machineSelector.width, UM.Theme.getSize("machine_selector_widget_content").width) implicitHeight: Math.min(machineSelectorList.contentHeight + separator.height + buttonRow.height, UM.Theme.getSize("machine_selector_widget_content").height) //Maximum height is the theme entry. MachineSelectorList { @@ -224,6 +224,9 @@ Cura.ExpandablePopup anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter + anchors.left: parent.left + anchors.right: parent.right + padding: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").width @@ -236,7 +239,7 @@ Cura.ExpandablePopup // The maximum width of the button is half of the total space, minus the padding of the parent, the left // padding of the component and half the spacing because of the space between buttons. fixedWidthMode: true - width: UM.Theme.getSize("machine_selector_widget_content").width / 2 - leftPadding + width: buttonRow.width / 2 - leftPadding * 1.5 onClicked: { toggleContent() @@ -253,7 +256,7 @@ Cura.ExpandablePopup fixedWidthMode: true // The maximum width of the button is half of the total space, minus the padding of the parent, the right // padding of the component and half the spacing because of the space between buttons. - width: UM.Theme.getSize("machine_selector_widget_content").width / 2 - leftPadding + width: buttonRow.width / 2 - rightPadding * 1.5 onClicked: { toggleContent() diff --git a/resources/qml/PrinterSelector/MachineSelectorList.qml b/resources/qml/PrinterSelector/MachineSelectorList.qml index ae2706f9ab..06c2fdb40c 100644 --- a/resources/qml/PrinterSelector/MachineSelectorList.qml +++ b/resources/qml/PrinterSelector/MachineSelectorList.qml @@ -10,8 +10,8 @@ import Cura 1.0 as Cura ListView { id: listView - model: Cura.GlobalStacksModel {} - section.property: "hasRemoteConnection" + model: Cura.MachineListModel {} + section.property: "isOnline" property real contentHeight: childrenRect.height ScrollBar.vertical: UM.ScrollBar @@ -21,7 +21,7 @@ ListView section.delegate: UM.Label { - text: section == "true" ? catalog.i18nc("@label", "Connected printers") : catalog.i18nc("@label", "Preset printers") + text: section == "true" ? catalog.i18nc("@label", "Connected printers") : catalog.i18nc("@label", "Other printers") width: parent.width - scrollBar.width height: UM.Theme.getSize("action_button").height leftPadding: UM.Theme.getSize("default_margin").width @@ -29,13 +29,10 @@ ListView color: UM.Theme.getColor("text_medium") } - delegate: MachineSelectorButton + delegate: MachineListButton { text: model.name ? model.name : "" width: listView.width - scrollBar.width - outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - - checked: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.id == model.id : false onClicked: { diff --git a/resources/qml/qmldir b/resources/qml/qmldir index a47d85545b..6ec3ca91c8 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -2,6 +2,7 @@ module Cura MachineSelector 1.0 MachineSelector.qml MachineSelectorButton 1.0 MachineSelectorButton.qml +MachineListButton 1.0 MachineListButton.qml CustomConfigurationSelector 1.0 CustomConfigurationSelector.qml PrintSetupSelector 1.0 PrintSetupSelector.qml ProfileOverview 1.6 ProfileOverview.qml diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index e7622bc685..809bcfdee8 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -564,6 +564,9 @@ "medium_button": [2.5, 2.5], "medium_button_icon": [2, 2], + "large_button": [3.0, 3.0], + "large_button_icon": [2.8, 2.8], + "context_menu": [20, 2], "icon_indicator": [1, 1],