diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index afdffa1f77..dee468f069 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -3,11 +3,13 @@ import os.path import urllib +import uuid from typing import Dict, Union from PyQt5.QtCore import QObject, QUrl, QVariant from UM.FlameProfiler import pyqtSlot from PyQt5.QtWidgets import QMessageBox +from UM.Util import parseBool from UM.PluginRegistry import PluginRegistry from UM.SaveFile import SaveFile @@ -671,6 +673,9 @@ class ContainerManager(QObject): return new_change_instances + ## Create a duplicate of a material, which has the same GUID and base_file metadata + # + # \return \type{str} the id of the newly created container. @pyqtSlot(str, result = str) def duplicateMaterial(self, material_id: str) -> str: containers = self._container_registry.findInstanceContainers(id=material_id) @@ -693,6 +698,104 @@ class ContainerManager(QObject): duplicated_container.deserialize(f.read()) duplicated_container.setDirty(True) self._container_registry.addContainer(duplicated_container) + return self._getMaterialContainerIdForActiveMachine(new_id) + + ## Create a new material by cloning Generic PLA and setting the GUID to something unqiue + # + # \return \type{str} the id of the newly created container. + @pyqtSlot(result = str) + def createMaterial(self) -> str: + # Ensure all settings are saved. + Application.getInstance().saveSettings() + + containers = self._container_registry.findInstanceContainers(id="generic_pla") + if not containers: + Logger.log("d", "Unable to create a new material by cloning generic_pla, because it doesn't exist.") + return "" + + # Create a new ID & container to hold the data. + new_id = self._container_registry.uniqueName("custom_material") + container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer + duplicated_container = container_type(new_id) + + # Instead of duplicating we load the data from the basefile again. + # This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile + # are also correctly created. + with open(containers[0].getPath(), encoding="utf-8") as f: + duplicated_container.deserialize(f.read()) + + duplicated_container.setMetaDataEntry("GUID", str(uuid.uuid4())) + duplicated_container.setMetaDataEntry("brand", catalog.i18nc("@label", "Custom")) + duplicated_container.setMetaDataEntry("material", catalog.i18nc("@label", "Custom")) + duplicated_container.setName(catalog.i18nc("@label", "Custom Material")) + + self._container_registry.addContainer(duplicated_container) + return self._getMaterialContainerIdForActiveMachine(new_id) + + ## Find the id of a material container based on the new material + # Utilty function that is shared between duplicateMaterial and createMaterial + # + # \param base_file \type{str} the id of the created container. + def _getMaterialContainerIdForActiveMachine(self, base_file): + global_stack = Application.getInstance().getGlobalContainerStack() + if not global_stack: + return base_file + + has_machine_materials = parseBool(global_stack.getMetaDataEntry("has_machine_materials", default = False)) + has_variant_materials = parseBool(global_stack.getMetaDataEntry("has_variant_materials", default = False)) + has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False)) + if has_machine_materials or has_variant_materials: + if has_variants: + materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId) + else: + materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId()) + + if materials: + return materials[0].getId() + + Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file) + return "" # do not activate a new material if a container can not be found + + return base_file + + ## Get a list of materials that have the same GUID as the reference material + # + # \param material_id \type{str} the id of the material for which to get the linked materials. + # \return \type{list} a list of names of materials with the same GUID + @pyqtSlot(str, result = "QStringList") + def getLinkedMaterials(self, material_id: str): + containers = self._container_registry.findInstanceContainers(id=material_id) + if not containers: + Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id) + return [] + + material_container = containers[0] + material_base_file = material_container.getMetaDataEntry("base_file", "") + material_guid = material_container.getMetaDataEntry("GUID", "") + if not material_guid: + Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id) + return [] + + containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_guid) + linked_material_names = [] + for container in containers: + if container.getId() in [material_id, material_base_file] or container.getMetaDataEntry("base_file") != container.getId(): + continue + + linked_material_names.append(container.getName()) + return linked_material_names + + ## Unlink a material from all other materials by creating a new GUID + # \param material_id \type{str} the id of the material to create a new GUID for. + @pyqtSlot(str) + def unlinkMaterial(self, material_id: str): + containers = self._container_registry.findInstanceContainers(id=material_id) + if not containers: + Logger.log("d", "Unable to make the material with id %s unique, because it doesn't exist.", material_id) + return "" + + containers[0].setMetaDataEntry("GUID", str(uuid.uuid4())) + ## Get the singleton instance for this class. @classmethod diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index 226fd349bf..9290126502 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -24,66 +24,89 @@ TabView property double spoolLength: calculateSpoolLength() property real costPerMeter: calculateCostPerMeter() + property bool reevaluateLinkedMaterials: false + property string linkedMaterialNames: + { + if (reevaluateLinkedMaterials) + { + reevaluateLinkedMaterials = false; + } + if(!base.containerId || !base.editingEnabled) + { + return "" + } + var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.containerId); + return linkedMaterials.join(", "); + } + Tab { title: catalog.i18nc("@title","Information") - anchors - { - leftMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_margin").height - bottomMargin: UM.Theme.getSize("default_margin").height - rightMargin: 0 - } + anchors.margins: UM.Theme.getSize("default_margin").width ScrollView { + id: scrollView anchors.fill: parent horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff flickableItem.flickableDirection: Flickable.VerticalFlick + frameVisible: true + + property real columnWidth: Math.floor(viewport.width * 0.5) - UM.Theme.getSize("default_margin").width Flow { id: containerGrid - width: base.width; + x: UM.Theme.getSize("default_margin").width + y: UM.Theme.getSize("default_lining").height - property real rowHeight: textField.height; + width: base.width + property real rowHeight: textField.height + UM.Theme.getSize("default_lining").height - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Display Name") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Display Name") } ReadOnlyTextField { id: displayNameTextField; - width: base.secondColumnWidth; + width: scrollView.columnWidth; text: properties.name; readOnly: !base.editingEnabled; onEditingFinished: base.setName(properties.name, text) } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Brand") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Brand") } ReadOnlyTextField { id: textField; - width: base.secondColumnWidth; + width: scrollView.columnWidth; text: properties.supplier; readOnly: !base.editingEnabled; - onEditingFinished: base.setMetaDataEntry("brand", properties.supplier, text) + onEditingFinished: + { + base.setMetaDataEntry("brand", properties.supplier, text); + pane.objectList.currentIndex = pane.getIndexById(base.containerId); + } } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") } ReadOnlyTextField { - width: base.secondColumnWidth; + width: scrollView.columnWidth; text: properties.material_type; readOnly: !base.editingEnabled; - onEditingFinished: base.setMetaDataEntry("material", properties.material_type, text) + onEditingFinished: + { + base.setMetaDataEntry("material", properties.material_type, text); + pane.objectList.currentIndex = pane.getIndexById(base.containerId) + } } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") } Row { - width: base.secondColumnWidth; + width: scrollView.columnWidth; height: parent.rowHeight; spacing: UM.Theme.getSize("default_margin").width/2 @@ -115,11 +138,11 @@ TabView Label { width: parent.width; height: parent.rowHeight; font.bold: true; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Properties") } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Density") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Density") } ReadOnlySpinBox { id: densitySpinBox - width: base.secondColumnWidth + width: scrollView.columnWidth value: properties.density decimals: 2 suffix: " g/cm³" @@ -130,11 +153,11 @@ TabView onValueChanged: updateCostPerMeter() } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Diameter") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Diameter") } ReadOnlySpinBox { id: diameterSpinBox - width: base.secondColumnWidth + width: scrollView.columnWidth value: properties.diameter decimals: 2 suffix: " mm" @@ -145,11 +168,11 @@ TabView onValueChanged: updateCostPerMeter() } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament Cost") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament Cost") } SpinBox { id: spoolCostSpinBox - width: base.secondColumnWidth + width: scrollView.columnWidth value: base.getMaterialPreferenceValue(properties.guid, "spool_cost") prefix: base.currency + " " decimals: 2 @@ -161,11 +184,11 @@ TabView } } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament weight") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament weight") } SpinBox { id: spoolWeightSpinBox - width: base.secondColumnWidth + width: scrollView.columnWidth value: base.getMaterialPreferenceValue(properties.guid, "spool_weight") suffix: " g" stepSize: 100 @@ -178,24 +201,45 @@ TabView } } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament length") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament length") } Label { - width: base.secondColumnWidth + width: scrollView.columnWidth text: "~ %1 m".arg(Math.round(base.spoolLength)) verticalAlignment: Qt.AlignVCenter height: parent.rowHeight } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Cost per Meter") } + Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Cost per Meter") } Label { - width: base.secondColumnWidth + width: scrollView.columnWidth text: "~ %1 %2/m".arg(base.costPerMeter.toFixed(2)).arg(base.currency) verticalAlignment: Qt.AlignVCenter height: parent.rowHeight } + Item { width: parent.width; height: UM.Theme.getSize("default_margin").height; visible: unlinkMaterialButton.visible } + Label + { + width: 2 * scrollView.columnWidth + verticalAlignment: Qt.AlignVCenter + text: catalog.i18nc("@label", "This material is linked to %1 and shares some of its properties.").arg(base.linkedMaterialNames) + wrapMode: Text.WordWrap + visible: unlinkMaterialButton.visible + } + Button + { + id: unlinkMaterialButton + text: catalog.i18nc("@label", "Unlink Material") + visible: base.linkedMaterialNames != "" + onClicked: + { + Cura.ContainerManager.unlinkMaterial(base.containerId) + base.reevaluateLinkedMaterials = true + } + } + Item { width: parent.width; height: UM.Theme.getSize("default_margin").height } Label { width: parent.width; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Description") } @@ -203,7 +247,7 @@ TabView ReadOnlyTextArea { text: properties.description; - width: base.firstColumnWidth + base.secondColumnWidth + width: 2 * scrollView.columnWidth wrapMode: Text.WordWrap readOnly: !base.editingEnabled; @@ -216,13 +260,15 @@ TabView ReadOnlyTextArea { text: properties.adhesion_info; - width: base.firstColumnWidth + base.secondColumnWidth + width: 2 * scrollView.columnWidth wrapMode: Text.WordWrap readOnly: !base.editingEnabled; onEditingFinished: base.setMetaDataEntry("adhesion_info", properties.adhesion_info, text) } + + Item { width: parent.width; height: UM.Theme.getSize("default_margin").height } } function updateCostPerMeter() @@ -266,8 +312,10 @@ TabView { id: label width: base.firstColumnWidth; - height: spinBox.height + height: spinBox.height + UM.Theme.getSize("default_lining").height text: model.label + elide: Text.ElideRight + verticalAlignment: Qt.AlignVCenter } ReadOnlySpinBox { @@ -380,6 +428,7 @@ TabView Cura.ContainerManager.setContainerName(base.containerId, new_value); // update material name label. not so pretty, but it works materialProperties.name = new_value; + pane.objectList.currentIndex = pane.getIndexById(base.containerId) } } } diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 08cb6d4d13..c46b482d10 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -14,6 +14,12 @@ UM.ManagementPage title: catalog.i18nc("@title:tab", "Materials"); + Component.onCompleted: + { + // Workaround to make sure all of the items are visible + objectList.positionViewAtBeginning(); + } + model: UM.InstanceContainersModel { filter: @@ -81,6 +87,7 @@ UM.ManagementPage anchors.fill: parent; onClicked: { + forceActiveFocus(); if(!parent.ListView.isCurrentItem) { parent.ListView.view.currentIndex = index; @@ -91,9 +98,11 @@ UM.ManagementPage } activeId: Cura.MachineManager.activeMaterialId - activeIndex: { + activeIndex: getIndexById(activeId) + function getIndexById(material_id) + { for(var i = 0; i < model.rowCount(); i++) { - if (model.getItem(i).id == Cura.MachineManager.activeMaterialId) { + if (model.getItem(i).id == material_id) { return i; } } @@ -135,6 +144,24 @@ UM.ManagementPage } }, Button + { + text: catalog.i18nc("@action:button", "Create") + iconName: "list-add" + onClicked: + { + var material_id = Cura.ContainerManager.createMaterial() + if(material_id == "") + { + return + } + if(Cura.MachineManager.hasMaterials) + { + Cura.MachineManager.setActiveMaterial(material_id) + } + base.objectList.currentIndex = base.getIndexById(material_id); + } + }, + Button { text: catalog.i18nc("@action:button", "Duplicate"); iconName: "list-add"; @@ -152,6 +179,7 @@ UM.ManagementPage { Cura.MachineManager.setActiveMaterial(material_id) } + base.objectList.currentIndex = base.getIndexById(material_id); } }, Button @@ -206,6 +234,8 @@ UM.ManagementPage properties: materialProperties containerId: base.currentItem != null ? base.currentItem.id : "" + + property alias pane: base } QtObject @@ -251,6 +281,10 @@ UM.ManagementPage { Cura.ContainerManager.removeContainer(containers[i]) } + if(base.objectList.currentIndex > 0) + { + base.objectList.currentIndex--; + } currentItem = base.model.getItem(base.objectList.currentIndex) // Refresh the current item. } } diff --git a/resources/qml/Preferences/ReadOnlyTextArea.qml b/resources/qml/Preferences/ReadOnlyTextArea.qml index 1c457eb5d2..db92c7d2b0 100644 --- a/resources/qml/Preferences/ReadOnlyTextArea.qml +++ b/resources/qml/Preferences/ReadOnlyTextArea.qml @@ -45,17 +45,4 @@ Item } } } - - Label - { - visible: base.readOnly - text: textArea.text - - anchors.fill: parent - anchors.margins: textArea.__style ? textArea.__style.textMargin : 4 - - color: palette.buttonText - } - - SystemPalette { id: palette } }