diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 97c849144d..c69cf91433 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,7 +6,7 @@ Before filing, PLEASE check if the issue already exists (either open or closed) Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for. It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker. -Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too. +Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that github accepts uploading the file. Otherwise we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too. Thank you for using Cura! --> @@ -26,6 +26,9 @@ Thank you for using Cura! **Display Driver** +**Printer** + + **Steps to Reproduce** diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index de0c68d60a..5c09a4f060 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -3,6 +3,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty +from UM.Application import Application from UM.Qt.ListModel import ListModel @@ -25,6 +26,8 @@ class BaseMaterialsModel(ListModel): def __init__(self, parent = None): super().__init__(parent) + self._application = Application.getInstance() + self._machine_manager = self._application.getMachineManager() self.addRoleName(self.RootMaterialIdRole, "root_material_id") self.addRoleName(self.IdRole, "id") @@ -35,12 +38,33 @@ class BaseMaterialsModel(ListModel): self.addRoleName(self.ContainerNodeRole, "container_node") self._extruder_position = 0 + self._extruder_stack = None + + self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) + + def _updateExtruderStack(self): + global_stack = self._machine_manager.activeMachine + if global_stack is None: + return + + if self._extruder_stack is not None: + self._extruder_stack.pyqtContainersChanged.disconnect(self._update) + self._extruder_stack = global_stack.extruders.get(str(self._extruder_position)) + if self._extruder_stack is not None: + self._extruder_stack.pyqtContainersChanged.connect(self._update) def setExtruderPosition(self, position: int): if self._extruder_position != position: self._extruder_position = position + self._updateExtruderStack() self.extruderPositionChanged.emit() @pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged) def extruderPosition(self) -> int: - return self._extruder_positoin + return self._extruder_position + + # + # This is an abstract method that needs to be implemented by + # + def _update(self): + pass diff --git a/cura/Machines/Models/BrandMaterialsModel.py b/cura/Machines/Models/BrandMaterialsModel.py index 6628d924f1..2ef1425986 100644 --- a/cura/Machines/Models/BrandMaterialsModel.py +++ b/cura/Machines/Models/BrandMaterialsModel.py @@ -120,12 +120,19 @@ class BrandMaterialsModel(ListModel): material_type_item = {"name": material_type, "colors": BaseMaterialsModel(self)} material_type_item["colors"].clear() + + # Sort materials by name + material_list = sorted(material_list, key = lambda x: x["name"]) material_type_item["colors"].setItems(material_list) material_type_item_list.append(material_type_item) + # Sort material type by name + material_type_item_list = sorted(material_type_item_list, key = lambda x: x["name"]) brand_item["materials"].setItems(material_type_item_list) brand_item_list.append(brand_item) + # Sort brand by name + brand_item_list = sorted(brand_item_list, key = lambda x: x["name"]) self.setItems(brand_item_list) diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index d7077d3d85..f02e8b4db5 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -19,8 +19,6 @@ class ObjectsModel(ListModel): self._build_plate_number = -1 - self._stacks_have_errors = None # type:Optional[bool] - def setActiveBuildPlate(self, nr): self._build_plate_number = nr self._update() @@ -69,11 +67,3 @@ class ObjectsModel(ListModel): @staticmethod def createObjectsModel(): return ObjectsModel() - - ## Check if none of the model's stacks contain error states - # The setting applied for the settings per model - def stacksHaveErrors(self) -> bool: - return bool(self._stacks_have_errors) - - def setStacksHaveErrors(self, value): - self._stacks_have_errors = value \ No newline at end of file diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 6e0679e11c..053b9d033a 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -968,30 +968,38 @@ class MachineManager(QObject): ## Update current quality type and machine after setting material def _updateQualityWithMaterial(self): - current_quality = None + Logger.log("i", "Updating quality/quality_changes due to material change") + current_quality_type = None if self._current_quality_group: - current_quality = self._current_quality_group.quality_type - quality_manager = Application.getInstance()._quality_manager - candidate_quality_groups = quality_manager.getQualityGroups(self._global_container_stack) + current_quality_type = self._current_quality_group.quality_type + candidate_quality_groups = self._quality_manager.getQualityGroups(self._global_container_stack) available_quality_types = {qt for qt, g in candidate_quality_groups.items() if g.is_available} + Logger.log("d", "Current quality type = [%s]", current_quality_type) if not self.activeMaterialsCompatible(): + Logger.log("i", "Active materials are not compatible, setting all qualities to empty (Not Supported).") self._setEmptyQuality() return if not available_quality_types: + Logger.log("i", "No available quality types found, setting all qualities to empty (Not Supported).") self._setEmptyQuality() return - if current_quality in available_quality_types: - self._setQualityGroup(candidate_quality_groups[current_quality], empty_quality_changes = False) + if current_quality_type in available_quality_types: + Logger.log("i", "Current available quality type [%s] is available, applying changes.", current_quality_type) + self._setQualityGroup(candidate_quality_groups[current_quality_type], empty_quality_changes = False) return + # The current quality type is not available so we use the preferred quality type if it's available, + # otherwise use one of the available quality types. quality_type = sorted(list(available_quality_types))[0] preferred_quality_type = self._global_container_stack.getMetaDataEntry("preferred_quality_type") if preferred_quality_type in available_quality_types: quality_type = preferred_quality_type + Logger.log("i", "The current quality type [%s] is not available, switching to [%s] instead", + current_quality_type, quality_type) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) def _updateMaterialWithVariant(self, position: Optional[str]): @@ -1006,9 +1014,8 @@ class MachineManager(QObject): current_material_base_name = extruder.material.getMetaDataEntry("base_file") current_variant_name = extruder.variant.getMetaDataEntry("name") - material_manager = Application.getInstance()._material_manager material_diameter = self._global_container_stack.getProperty("material_diameter", "value") - candidate_materials = material_manager.getAvailableMaterials( + candidate_materials = self._material_manager.getAvailableMaterials( self._global_container_stack.definition.getId(), current_variant_name, material_diameter) @@ -1065,7 +1072,7 @@ class MachineManager(QObject): # See if we need to show the Discard or Keep changes screen if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: - Application.getInstance().discardOrKeepProfileChanges() + self._application.discardOrKeepProfileChanges() @pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged) def activeQualityGroup(self): @@ -1079,7 +1086,7 @@ class MachineManager(QObject): # See if we need to show the Discard or Keep changes screen if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: - Application.getInstance().discardOrKeepProfileChanges() + self._application.discardOrKeepProfileChanges() @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) def activeQualityChangesGroup(self): diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 6e98f014dc..24d94e4955 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -9,8 +9,7 @@ from UM.Signal import Signal, signalemitter from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Logger import Logger -from UM.Settings.Validator import ValidatorState -from PyQt5.QtCore import QTimer + from UM.Application import Application from cura.Settings.PerObjectContainerStack import PerObjectContainerStack @@ -40,10 +39,6 @@ class SettingOverrideDecorator(SceneNodeDecorator): self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() self._is_non_printing_mesh = False - self._error_check_timer = QTimer() - self._error_check_timer.setInterval(250) - self._error_check_timer.setSingleShot(True) - self._error_check_timer.timeout.connect(self._checkStackForErrors) self._stack.propertyChanged.connect(self._onSettingChanged) @@ -99,21 +94,9 @@ class SettingOverrideDecorator(SceneNodeDecorator): # Trigger slice/need slicing if the value has changed. if property_name == "value": self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) - if not self._is_non_printing_mesh: - # self._error_check_timer.start() - self._checkStackForErrors() - Application.getInstance().getBackend().needsSlicing() - Application.getInstance().getBackend().tickle() - def _checkStackForErrors(self): - hasErrors = False; - for key in self._stack.getAllKeys(): - validation_state = self._stack.getProperty(key, "validationState") - if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): - Logger.log("w", "Setting Per Object %s is not valid.", key) - hasErrors = True - break - Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors) + Application.getInstance().getBackend().needsSlicing() + Application.getInstance().getBackend().tickle() ## Makes sure that the stack upon which the container stack is placed is # kept up to date. diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 8b7205f8b2..afbc816cc5 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -136,14 +136,10 @@ class StartSliceJob(Job): self.setResult(StartJobResult.MaterialIncompatible) return - # Validate settings per selectable model - if Application.getInstance().getObjectsModel().stacksHaveErrors(): - self.setResult(StartJobResult.ObjectSettingError) - return # Don't slice if there is a per object setting with an error value. for node in DepthFirstIterator(self._scene.getRoot()): - if node.isSelectable(): + if type(node) is not CuraSceneNode or not node.isSelectable(): continue if self._checkStackForErrors(node.callDecoration("getStack")): diff --git a/plugins/PerObjectSettingsTool/PerObjectItem.qml b/plugins/PerObjectSettingsTool/PerObjectItem.qml index 1317c00b19..559ad2bf81 100644 --- a/plugins/PerObjectSettingsTool/PerObjectItem.qml +++ b/plugins/PerObjectSettingsTool/PerObjectItem.qml @@ -25,20 +25,7 @@ UM.TooltipArea onClicked: { - // Important first set visible and then subscribe - // otherwise the setting is not yet in list - // For unsubscribe is important first remove the subscription and then - // set as invisible - if(checked) - { - addedSettingsModel.setVisible(model.key, checked); - UM.ActiveTool.triggerActionWithData("subscribeForSettingValidation", model.key) - } - else - { - UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key) - addedSettingsModel.setVisible(model.key, checked); - } + addedSettingsModel.setVisible(model.key, checked); UM.ActiveTool.forceUpdate(); } } diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index e72e1224df..03a2ce1bf4 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -240,10 +240,7 @@ Item { width: Math.round(UM.Theme.getSize("setting").height / 2) height: UM.Theme.getSize("setting").height - onClicked: { - addedSettingsModel.setVisible(model.key, false) - UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key) - } + onClicked: addedSettingsModel.setVisible(model.key, false) style: ButtonStyle { diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index b671db48fb..d2db5ff420 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -10,10 +10,7 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.ExtruderManager import ExtruderManager from UM.Settings.SettingInstance import SettingInstance from UM.Event import Event -from UM.Settings.Validator import ValidatorState -from UM.Logger import Logger -from PyQt5.QtCore import QTimer ## This tool allows the user to add & change settings per node in the scene. # The settings per object are kept in a ContainerStack, which is linked to a node by decorator. @@ -37,12 +34,6 @@ class PerObjectSettingsTool(Tool): self._onGlobalContainerChanged() Selection.selectionChanged.connect(self._updateEnabled) - self._scene = Application.getInstance().getController().getScene() - - self._error_check_timer = QTimer() - self._error_check_timer.setInterval(250) - self._error_check_timer.setSingleShot(True) - self._error_check_timer.timeout.connect(self._updateStacksHaveErrors) def event(self, event): super().event(event) @@ -151,65 +142,3 @@ class PerObjectSettingsTool(Tool): else: self._single_model_selected = True Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode and self._single_model_selected) - - - def _onPropertyChanged(self, key: str, property_name: str) -> None: - if property_name == "validationState": - # self._error_check_timer.start() - return - - def _updateStacksHaveErrors(self) -> None: - return - # self._checkStacksHaveErrors() - - - def _checkStacksHaveErrors(self): - - for node in DepthFirstIterator(self._scene.getRoot()): - - # valdiate only objects which can be selected because the settings per object - # can be applied only for them - if not node.isSelectable(): - continue - - hasErrors = self._checkStackForErrors(node.callDecoration("getStack")) - Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors) - - #If any of models has an error then no reason check next objects on the build plate - if hasErrors: - break - - - def _checkStackForErrors(self, stack): - print("checking for errors") - if stack is None: - return False - - for key in stack.getAllKeys(): - validation_state = stack.getProperty(key, "validationState") - if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): - Logger.log("w", "Setting Per Object %s is not valid.", key) - return True - return False - - def subscribeForSettingValidation(self, setting_name): - selected_object = Selection.getSelectedObject(0) - stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway. - if not stack: - return "" - - settings = stack.getTop() - setting_instance = settings.getInstance(setting_name) - if setting_instance: - setting_instance.propertyChanged.connect(self._onPropertyChanged) - - def unsubscribeForSettingValidation(self, setting_name): - selected_object = Selection.getSelectedObject(0) - stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway. - if not stack: - return "" - - settings = stack.getTop() - setting_instance = settings.getInstance(setting_name) - if setting_instance: - setting_instance.propertyChanged.disconnect(self._onPropertyChanged) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c0d538bb78..73acc02cae 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -194,6 +194,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # the "reply" should be disconnected if self._latest_reply_handler: self._latest_reply_handler.disconnect() + self._latest_reply_handler = None @pyqtSlot() diff --git a/plugins/XRayView/XRayPass.py b/plugins/XRayView/XRayPass.py index 38c88a256e..a75d393b35 100644 --- a/plugins/XRayView/XRayPass.py +++ b/plugins/XRayView/XRayPass.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os.path @@ -10,7 +10,7 @@ from UM.View.RenderPass import RenderPass from UM.View.RenderBatch import RenderBatch from UM.View.GL.OpenGL import OpenGL -from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator class XRayPass(RenderPass): @@ -27,7 +27,7 @@ class XRayPass(RenderPass): batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive) for node in DepthFirstIterator(self._scene.getRoot()): - if type(node) is SceneNode and node.getMeshData() and node.isVisible(): + if isinstance(node, CuraSceneNode) and node.getMeshData() and node.isVisible(): batch.addItem(node.getWorldTransformation(), node.getMeshData()) self.bind() diff --git a/plugins/XRayView/xray_composite.shader b/plugins/XRayView/xray_composite.shader index 82dca52cf9..0a8f6364d7 100644 --- a/plugins/XRayView/xray_composite.shader +++ b/plugins/XRayView/xray_composite.shader @@ -13,9 +13,9 @@ vertex = } fragment = - uniform sampler2D u_layer0; - uniform sampler2D u_layer1; - uniform sampler2D u_layer2; + uniform sampler2D u_layer0; //Default pass. + uniform sampler2D u_layer1; //Selection pass. + uniform sampler2D u_layer2; //X-ray pass. uniform vec2 u_offset[9]; @@ -83,9 +83,9 @@ vertex41core = fragment41core = #version 410 - uniform sampler2D u_layer0; - uniform sampler2D u_layer1; - uniform sampler2D u_layer2; + uniform sampler2D u_layer0; //Default pass. + uniform sampler2D u_layer1; //Selection pass. + uniform sampler2D u_layer2; //X-ray pass. uniform vec2 u_offset[9]; diff --git a/resources/qml/Menus/ProfileMenu.qml b/resources/qml/Menus/ProfileMenu.qml index 72cda13ca9..5b9a5a3b73 100644 --- a/resources/qml/Menus/ProfileMenu.qml +++ b/resources/qml/Menus/ProfileMenu.qml @@ -52,6 +52,7 @@ Menu { text: model.name checkable: model.available + enabled: model.available checked: Cura.MachineManager.activeQualityOrQualityChangesName == model.name exclusiveGroup: group onTriggered: Cura.MachineManager.setQualityChangesGroup(model.quality_changes_group) diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index c987880305..d2f653e650 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -47,6 +47,19 @@ TabView return Math.round(diameter); } + // This trick makes sure to make all fields lose focus so their onEditingFinished will be triggered + // and modified values will be saved. This can happen when a user changes a value and then closes the + // dialog directly. + // + // Please note that somehow this callback is ONLY triggered when visible is false. + onVisibleChanged: + { + if (!visible) + { + base.focus = false; + } + } + Tab { title: catalog.i18nc("@title", "Information") diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 4a6d07df81..553cfe0423 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -52,6 +52,24 @@ Item return base.currentItem.root_material_id == root_material_id; } + Component.onCompleted: + { + // Select the activated material when this page shows up + const extruder_position = Cura.ExtruderManager.activeExtruderIndex; + const active_root_material_id = Cura.MachineManager.currentRootMaterialId[extruder_position]; + var itemIndex = -1; + for (var i = 0; i < materialsModel.rowCount(); ++i) + { + var item = materialsModel.getItem(i); + if (item.root_material_id == active_root_material_id) + { + itemIndex = i; + break; + } + } + materialListView.currentIndex = itemIndex; + } + Row // Button Row { id: buttonRow