diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 411a78948b..daf72d5aad 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -133,12 +133,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # In Cura 2.5 and 2.6, the empty profiles used to have those long names self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]} - self._is_same_machine_type = False self._old_new_materials = {} # type: Dict[str, str] self._machine_info = None def _clearState(self): - self._is_same_machine_type = False self._id_mapping = {} self._old_new_materials = {} self._machine_info = None @@ -229,6 +227,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Read definition containers # machine_definition_id = None + updatable_machines = [] machine_definition_container_count = 0 extruder_definition_container_count = 0 definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] @@ -245,6 +244,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): definition_container_type = definition_container.get("type") if definition_container_type == "machine": machine_definition_id = container_id + machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id) + if machine_definition_containers: + updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]] machine_type = definition_container["name"] variant_type_name = definition_container.get("variants_name", variant_type_name) @@ -386,8 +388,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_definition_id = id_list[7] stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine") - self._is_same_machine_type = True existing_global_stack = None + global_stack = None if stacks: global_stack = stacks[0] @@ -400,7 +402,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if global_stack.getContainer(index).getId() != container_id: machine_conflict = True break - self._is_same_machine_type = global_stack.definition.getId() == machine_definition_id + + if updatable_machines and not containers_found_dict["machine"]: + containers_found_dict["machine"] = True # Get quality type parser = ConfigParser(interpolation = None) @@ -485,7 +489,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if intent_id not in ("empty", "empty_intent"): extruder_info.intent_info = instance_container_info_dict[intent_id] - if not machine_conflict and containers_found_dict["machine"]: + if not machine_conflict and containers_found_dict["machine"] and global_stack: if int(position) >= len(global_stack.extruderList): continue @@ -555,9 +559,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._machine_info.custom_quality_name = quality_name self._machine_info.intent_category = intent_category - if machine_conflict and not self._is_same_machine_type: - machine_conflict = False - is_printer_group = False if machine_conflict: group_name = existing_global_stack.getMetaDataEntry("group_name") @@ -578,6 +579,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes) self._dialog.setNumUserSettings(num_user_settings) self._dialog.setActiveMode(active_mode) + self._dialog.setUpdatableMachines(updatable_machines) self._dialog.setMachineName(machine_name) self._dialog.setMaterialLabels(material_labels) self._dialog.setMachineType(machine_type) @@ -658,8 +660,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): application.expandedCategoriesChanged.emit() # Notify the GUI of the change - # If a machine with the same name is of a different type, always create a new one. - if not self._is_same_machine_type or self._resolve_strategies["machine"] != "override": + # If there are no machines of the same type, create a new machine. + if self._resolve_strategies["machine"] != "override" or self._dialog.updatableMachinesModel.count <= 1: # We need to create a new machine machine_name = self._container_registry.uniqueName(self._machine_info.name) @@ -669,10 +671,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._container_registry.addContainer(global_stack) else: - # Find the machine - global_stacks = self._container_registry.findContainerStacks(name = self._machine_info.name, type = "machine") + # Find the machine which will be overridden + global_stacks = self._container_registry.findContainerStacks(name = self._dialog.getMachineToOverride(), type = "machine") if not global_stacks: - message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", "Project file {0} is made using profiles that are unknown to this version of Ultimaker Cura.", file_name)) + message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", + "Project file {0} is made using profiles that" + " are unknown to this version of Ultimaker Cura.", file_name)) message.show() self.setWorkspaceName("") return [], {} diff --git a/plugins/3MFReader/UpdatableMachinesModel.py b/plugins/3MFReader/UpdatableMachinesModel.py new file mode 100644 index 0000000000..a332c669e6 --- /dev/null +++ b/plugins/3MFReader/UpdatableMachinesModel.py @@ -0,0 +1,43 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Dict, List + +from PyQt5.QtCore import Qt + +from UM.Qt.ListModel import ListModel +from cura.Settings.GlobalStack import GlobalStack + +create_new_list_item = { + "id": "new", + "name": "Create new", + "displayName": "Create new", + "type": "default_option" # to make sure we are not mixing the "Create new" option with a printer with id "new" +} # type: Dict[str, str] + + +class UpdatableMachinesModel(ListModel): + """Model that holds cura packages. + + By setting the filter property the instances held by this model can be changed. + """ + + def __init__(self, parent = None) -> None: + super().__init__(parent) + + self.addRoleName(Qt.UserRole + 1, "id") + self.addRoleName(Qt.UserRole + 2, "name") + self.addRoleName(Qt.UserRole + 3, "displayName") + self.addRoleName(Qt.UserRole + 4, "type") # Either "default_option" or "machine" + + def update(self, machines: List[GlobalStack]) -> None: + items = [create_new_list_item] # type: List[Dict[str, str]] + + for machine in sorted(machines, key = lambda printer: printer.name): + items.append({ + "id": machine.id, + "name": machine.name, + "displayName": "Update " + machine.name, + "type": "machine" + }) + self.setItems(items) diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 3c97146583..8d59ec1339 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -1,5 +1,6 @@ -# Copyright (c) 2016 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import List, Optional, Dict, cast from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication from UM.FlameProfiler import pyqtSlot @@ -7,10 +8,15 @@ from UM.PluginRegistry import PluginRegistry from UM.Application import Application from UM.i18n import i18nCatalog from UM.Settings.ContainerRegistry import ContainerRegistry +from cura.Settings.GlobalStack import GlobalStack +from .UpdatableMachinesModel import UpdatableMachinesModel import os import threading import time + +from cura.CuraApplication import CuraApplication + i18n_catalog = i18nCatalog("cura") @@ -29,6 +35,7 @@ class WorkspaceDialog(QObject): "quality_changes": self._default_strategy, "definition_changes": self._default_strategy, "material": self._default_strategy} + self._override_machine = None self._visible = False self.showDialogSignal.connect(self.__show) @@ -51,6 +58,7 @@ class WorkspaceDialog(QObject): self._extruders = [] self._objects_on_plate = False self._is_printer_group = False + self._updatable_machines_model = UpdatableMachinesModel(self) machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() @@ -63,6 +71,7 @@ class WorkspaceDialog(QObject): qualityTypeChanged = pyqtSignal() intentNameChanged = pyqtSignal() machineNameChanged = pyqtSignal() + updatableMachinesChanged = pyqtSignal() materialLabelsChanged = pyqtSignal() objectsOnPlateChanged = pyqtSignal() numUserSettingsChanged = pyqtSignal() @@ -81,33 +90,33 @@ class WorkspaceDialog(QObject): self.isPrinterGroupChanged.emit() @pyqtProperty(str, notify=variantTypeChanged) - def variantType(self): + def variantType(self) -> str: return self._variant_type - def setVariantType(self, variant_type): + def setVariantType(self, variant_type: str) -> None: if self._variant_type != variant_type: self._variant_type = variant_type self.variantTypeChanged.emit() @pyqtProperty(str, notify=machineTypeChanged) - def machineType(self): + def machineType(self) -> str: return self._machine_type - def setMachineType(self, machine_type): + def setMachineType(self, machine_type: str) -> None: self._machine_type = machine_type self.machineTypeChanged.emit() - def setNumUserSettings(self, num_user_settings): + def setNumUserSettings(self, num_user_settings: int) -> None: if self._num_user_settings != num_user_settings: self._num_user_settings = num_user_settings self.numVisibleSettingsChanged.emit() @pyqtProperty(int, notify=numUserSettingsChanged) - def numUserSettings(self): + def numUserSettings(self) -> int: return self._num_user_settings @pyqtProperty(bool, notify=objectsOnPlateChanged) - def hasObjectsOnPlate(self): + def hasObjectsOnPlate(self) -> bool: return self._objects_on_plate def setHasObjectsOnPlate(self, objects_on_plate): @@ -116,10 +125,10 @@ class WorkspaceDialog(QObject): self.objectsOnPlateChanged.emit() @pyqtProperty("QVariantList", notify = materialLabelsChanged) - def materialLabels(self): + def materialLabels(self) -> List[str]: return self._material_labels - def setMaterialLabels(self, material_labels): + def setMaterialLabels(self, material_labels: List[str]) -> None: if self._material_labels != material_labels: self._material_labels = material_labels self.materialLabelsChanged.emit() @@ -134,36 +143,44 @@ class WorkspaceDialog(QObject): self.extrudersChanged.emit() @pyqtProperty(str, notify = machineNameChanged) - def machineName(self): + def machineName(self) -> str: return self._machine_name - def setMachineName(self, machine_name): + def setMachineName(self, machine_name: str) -> None: if self._machine_name != machine_name: self._machine_name = machine_name self.machineNameChanged.emit() + @pyqtProperty(QObject, notify = updatableMachinesChanged) + def updatableMachinesModel(self) -> UpdatableMachinesModel: + return cast(UpdatableMachinesModel, self._updatable_machines_model) + + def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None: + self._updatable_machines_model.update(updatable_machines) + self.updatableMachinesChanged.emit() + @pyqtProperty(str, notify=qualityTypeChanged) - def qualityType(self): + def qualityType(self) -> str: return self._quality_type - def setQualityType(self, quality_type): + def setQualityType(self, quality_type: str) -> None: if self._quality_type != quality_type: self._quality_type = quality_type self.qualityTypeChanged.emit() @pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged) - def numSettingsOverridenByQualityChanges(self): + def numSettingsOverridenByQualityChanges(self) -> int: return self._num_settings_overridden_by_quality_changes - def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes): + def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes: int) -> None: self._num_settings_overridden_by_quality_changes = num_settings_overridden_by_quality_changes self.numSettingsOverridenByQualityChangesChanged.emit() @pyqtProperty(str, notify=qualityNameChanged) - def qualityName(self): + def qualityName(self) -> str: return self._quality_name - def setQualityName(self, quality_name): + def setQualityName(self, quality_name: str) -> None: if self._quality_name != quality_name: self._quality_name = quality_name self.qualityNameChanged.emit() @@ -178,80 +195,87 @@ class WorkspaceDialog(QObject): self.intentNameChanged.emit() @pyqtProperty(str, notify=activeModeChanged) - def activeMode(self): + def activeMode(self) -> str: return self._active_mode - def setActiveMode(self, active_mode): + def setActiveMode(self, active_mode: int) -> None: if active_mode == 0: self._active_mode = i18n_catalog.i18nc("@title:tab", "Recommended") else: self._active_mode = i18n_catalog.i18nc("@title:tab", "Custom") self.activeModeChanged.emit() - @pyqtProperty(int, notify = hasVisibleSettingsFieldChanged) - def hasVisibleSettingsField(self): + @pyqtProperty(bool, notify = hasVisibleSettingsFieldChanged) + def hasVisibleSettingsField(self) -> bool: return self._has_visible_settings_field - def setHasVisibleSettingsField(self, has_visible_settings_field): + def setHasVisibleSettingsField(self, has_visible_settings_field: bool) -> None: self._has_visible_settings_field = has_visible_settings_field self.hasVisibleSettingsFieldChanged.emit() @pyqtProperty(int, constant = True) - def totalNumberOfSettings(self): + def totalNumberOfSettings(self) -> int: general_definition_containers = ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter") if not general_definition_containers: return 0 return len(general_definition_containers[0].getAllKeys()) @pyqtProperty(int, notify = numVisibleSettingsChanged) - def numVisibleSettings(self): + def numVisibleSettings(self) -> int: return self._num_visible_settings - def setNumVisibleSettings(self, num_visible_settings): + def setNumVisibleSettings(self, num_visible_settings: int) -> None: if self._num_visible_settings != num_visible_settings: self._num_visible_settings = num_visible_settings self.numVisibleSettingsChanged.emit() @pyqtProperty(bool, notify = machineConflictChanged) - def machineConflict(self): + def machineConflict(self) -> bool: return self._has_machine_conflict @pyqtProperty(bool, notify=qualityChangesConflictChanged) - def qualityChangesConflict(self): + def qualityChangesConflict(self) -> bool: return self._has_quality_changes_conflict @pyqtProperty(bool, notify=materialConflictChanged) - def materialConflict(self): + def materialConflict(self) -> bool: return self._has_material_conflict @pyqtSlot(str, str) - def setResolveStrategy(self, key, strategy): + def setResolveStrategy(self, key: str, strategy: Optional[str]) -> None: if key in self._result: self._result[key] = strategy + def getMachineToOverride(self) -> str: + return self._override_machine + + @pyqtSlot(str) + def setMachineToOverride(self, machine_name: str) -> None: + self._override_machine = machine_name + @pyqtSlot() - def closeBackend(self): + def closeBackend(self) -> None: """Close the backend: otherwise one could end up with "Slicing...""" Application.getInstance().getBackend().close() - def setMaterialConflict(self, material_conflict): + def setMaterialConflict(self, material_conflict: bool) -> None: if self._has_material_conflict != material_conflict: self._has_material_conflict = material_conflict self.materialConflictChanged.emit() - def setMachineConflict(self, machine_conflict): + def setMachineConflict(self, machine_conflict: bool) -> None: if self._has_machine_conflict != machine_conflict: self._has_machine_conflict = machine_conflict self.machineConflictChanged.emit() - def setQualityChangesConflict(self, quality_changes_conflict): + def setQualityChangesConflict(self, quality_changes_conflict: bool) -> None: if self._has_quality_changes_conflict != quality_changes_conflict: self._has_quality_changes_conflict = quality_changes_conflict self.qualityChangesConflictChanged.emit() - def getResult(self): - if "machine" in self._result and not self._has_machine_conflict: + def getResult(self) -> Dict[str, Optional[str]]: + if "machine" in self._result and self.updatableMachinesModel.count <= 1: self._result["machine"] = None if "quality_changes" in self._result and not self._has_quality_changes_conflict: self._result["quality_changes"] = None @@ -267,11 +291,13 @@ class WorkspaceDialog(QObject): return self._result - def _createViewFromQML(self): - path = os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url) - self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) + def _createViewFromQML(self) -> None: + three_mf_reader_path = PluginRegistry.getInstance().getPluginPath("3MFReader") + if three_mf_reader_path: + path = os.path.join(three_mf_reader_path, self._qml_url) + self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) - def show(self): + def show(self) -> None: # Emit signal so the right thread actually shows the view. if threading.current_thread() != threading.main_thread(): self._lock.acquire() @@ -284,7 +310,7 @@ class WorkspaceDialog(QObject): self.showDialogSignal.emit() @pyqtSlot() - def notifyClosed(self): + def notifyClosed(self) -> None: """Used to notify the dialog so the lock can be released.""" self._result = {} # The result should be cleared before hide, because after it is released the main thread lock @@ -294,7 +320,7 @@ class WorkspaceDialog(QObject): except: pass - def hide(self): + def hide(self) -> None: self._visible = False self._view.hide() try: @@ -303,7 +329,7 @@ class WorkspaceDialog(QObject): pass @pyqtSlot(bool) - def _onVisibilityChanged(self, visible): + def _onVisibilityChanged(self, visible: bool) -> None: if not visible: try: self._lock.release() @@ -311,17 +337,17 @@ class WorkspaceDialog(QObject): pass @pyqtSlot() - def onOkButtonClicked(self): + def onOkButtonClicked(self) -> None: self._view.hide() self.hide() @pyqtSlot() - def onCancelButtonClicked(self): + def onCancelButtonClicked(self) -> None: self._result = {} self._view.hide() self.hide() - def waitForClose(self): + def waitForClose(self) -> None: """Block thread until the dialog is closed.""" if self._visible: @@ -334,7 +360,7 @@ class WorkspaceDialog(QObject): time.sleep(1 / 50) QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - def __show(self): + def __show(self) -> None: if self._view is None: self._createViewFromQML() if self._view: diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index d0fd3d0846..5f67f54c39 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -2,7 +2,7 @@ // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 @@ -20,6 +20,7 @@ UM.Dialog property int comboboxHeight: 15 * screenScaleFactor property int spacerHeight: 10 * screenScaleFactor + property int doubleSpacerHeight: 20 * screenScaleFactor onClosing: manager.notifyClosed() onVisibleChanged: @@ -35,7 +36,7 @@ UM.Dialog Item { anchors.fill: parent - anchors.margins: 20 * screenScaleFactor + anchors.margins: 10 * screenScaleFactor UM.I18nCatalog { @@ -79,7 +80,7 @@ UM.Dialog } Item // Spacer { - height: spacerHeight + height: doubleSpacerHeight width: height } @@ -101,35 +102,53 @@ UM.Dialog } UM.TooltipArea { - id: machineResolveTooltip + id: machineResolveStrategyTooltip width: (parent.width / 3) | 0 height: visible ? comboboxHeight : 0 - visible: manager.machineConflict + visible: base.visible && machineResolveComboBox.model.count > 1 text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?") ComboBox { - model: ListModel - { - Component.onCompleted: - { - append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName}); - append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")}); - } - } - Connections - { - target: manager - onMachineNameChanged: - { - machineResolveComboBox.model.get(0).label = catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName; - } - } - textRole: "label" id: machineResolveComboBox + model: manager.updatableMachinesModel + visible: machineResolveStrategyTooltip.visible + textRole: "displayName" width: parent.width - onActivated: + onCurrentIndexChanged: { - manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + if (model.getItem(currentIndex).id == "new" + && model.getItem(currentIndex).type == "default_option") + { + manager.setResolveStrategy("machine", "new") + } + else + { + manager.setResolveStrategy("machine", "override") + manager.setMachineToOverride(model.getItem(currentIndex).id) + } + } + + onVisibleChanged: + { + if (!visible) {return} + + currentIndex = 0 + // If the project printer exists in Cura, set it as the default dropdown menu option. + // No need to check object 0, which is the "Create new" option + for (var i = 1; i < model.count; i++) + { + if (model.getItem(i).name == manager.machineName) + { + currentIndex = i + break + } + } + // The project printer does not exist in Cura. If there is at least one printer of the same + // type, select the first one, else set the index to "Create new" + if (currentIndex == 0 && model.count > 1) + { + currentIndex = 1 + } } } } @@ -168,7 +187,7 @@ UM.Dialog Item // Spacer { - height: spacerHeight + height: doubleSpacerHeight width: height } Row @@ -271,7 +290,7 @@ UM.Dialog } Item // Spacer { - height: spacerHeight + height: doubleSpacerHeight width: height } Row @@ -333,7 +352,7 @@ UM.Dialog Item // Spacer { - height: spacerHeight + height: doubleSpacerHeight width: height }