From e9965ab2a630ed4c8717b3e838769a39bd2d9195 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 26 Sep 2019 10:42:54 +0200 Subject: [PATCH 01/31] Revert the OneAtATimeIterator to the pre 06-2018 implementation. This seems like a better starting point to fix print head collisions, because we got less bug reports for it compared to the 2018 rewrite. CURA-6785 --- cura/OneAtATimeIterator.py | 227 ++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 132 deletions(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index a08f3ed2bf..ab97534ff4 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -1,149 +1,112 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import sys - -from shapely import affinity -from shapely.geometry import Polygon - -from UM.Scene.Iterator.Iterator import Iterator +from UM.Scene.Iterator import Iterator from UM.Scene.SceneNode import SceneNode +from functools import cmp_to_key +from UM.Application import Application - -# Iterator that determines the object print order when one-at a time mode is enabled. -# -# In one-at-a-time mode, only one extruder can be enabled to print. In order to maximize the number of objects we can -# print, we need to print from the corner that's closest to the extruder that's being used. Here is an illustration: -# -# +--------------------------------+ -# | | -# | | -# | | - Rectangle represents the complete print head including fans, etc. -# | X X | y - X's are the nozzles -# | (1) (2) | ^ -# | | | -# +--------------------------------+ +--> x -# -# In this case, the nozzles are symmetric, nozzle (1) is closer to the bottom left corner while (2) is closer to the -# bottom right. If we use nozzle (1) to print, then we better off printing from the bottom left corner so the print -# head will not collide into an object on its top-right side, which is a very large unused area. Following the same -# logic, if we are printing with nozzle (2), then it's better to print from the bottom-right side. -# -# This iterator determines the print order following the rules above. -# -class OneAtATimeIterator(Iterator): - +## Iterator that returns a list of nodes in the order that they need to be printed +# If there is no solution an empty list is returned. +# Take note that the list of nodes can have children (that may or may not contain mesh data) +class OneAtATimeIterator(Iterator.Iterator): def __init__(self, scene_node): - from cura.CuraApplication import CuraApplication - self._global_stack = CuraApplication.getInstance().getGlobalContainerStack() + super().__init__(scene_node) # Call super to make multiple inheritence work. + self._hit_map = [[]] self._original_node_list = [] - super().__init__(scene_node) # Call super to make multiple inheritance work. - - def getMachineNearestCornerToExtruder(self, global_stack): - head_and_fans_coordinates = global_stack.getHeadAndFansCoordinates() - - used_extruder = None - for extruder in global_stack.extruders.values(): - if extruder.isEnabled: - used_extruder = extruder - break - - extruder_offsets = [used_extruder.getProperty("machine_nozzle_offset_x", "value"), - used_extruder.getProperty("machine_nozzle_offset_y", "value")] - - # find the corner that's closest to the origin - min_distance2 = sys.maxsize - min_coord = None - for coord in head_and_fans_coordinates: - x = coord[0] - extruder_offsets[0] - y = coord[1] - extruder_offsets[1] - - distance2 = x**2 + y**2 - if distance2 <= min_distance2: - min_distance2 = distance2 - min_coord = coord - - return min_coord - - def _checkForCollisions(self) -> bool: - all_nodes = [] - for node in self._scene_node.getChildren(): - if not issubclass(type(node), SceneNode): - continue - convex_hull = node.callDecoration("getConvexHullHead") - if not convex_hull: - continue - - bounding_box = node.getBoundingBox() - if not bounding_box: - continue - from UM.Math.Polygon import Polygon - bounding_box_polygon = Polygon([[bounding_box.left, bounding_box.front], - [bounding_box.left, bounding_box.back], - [bounding_box.right, bounding_box.back], - [bounding_box.right, bounding_box.front]]) - - all_nodes.append({"node": node, - "bounding_box": bounding_box_polygon, - "convex_hull": convex_hull}) - - has_collisions = False - for i, node_dict in enumerate(all_nodes): - for j, other_node_dict in enumerate(all_nodes): - if i == j: - continue - if node_dict["bounding_box"].intersectsPolygon(other_node_dict["convex_hull"]): - has_collisions = True - break - - if has_collisions: - break - - return has_collisions - def _fillStack(self): - min_coord = self.getMachineNearestCornerToExtruder(self._global_stack) - transform_x = -int(round(min_coord[0] / abs(min_coord[0]))) - transform_y = -int(round(min_coord[1] / abs(min_coord[1]))) - - machine_size = [self._global_stack.getProperty("machine_width", "value"), - self._global_stack.getProperty("machine_depth", "value")] - - def flip_x(polygon): - tm2 = [-1, 0, 0, 1, 0, 0] - return affinity.affine_transform(affinity.translate(polygon, xoff = -machine_size[0]), tm2) - - def flip_y(polygon): - tm2 = [1, 0, 0, -1, 0, 0] - return affinity.affine_transform(affinity.translate(polygon, yoff = -machine_size[1]), tm2) - - if self._checkForCollisions(): - self._node_stack = [] - return - node_list = [] for node in self._scene_node.getChildren(): if not issubclass(type(node), SceneNode): continue - convex_hull = node.callDecoration("getConvexHull") - if convex_hull: - xmin = min(x for x, _ in convex_hull._points) - xmax = max(x for x, _ in convex_hull._points) - ymin = min(y for _, y in convex_hull._points) - ymax = max(y for _, y in convex_hull._points) + if node.callDecoration("getConvexHull"): + node_list.append(node) - convex_hull_polygon = Polygon.from_bounds(xmin, ymin, xmax, ymax) - if transform_x < 0: - convex_hull_polygon = flip_x(convex_hull_polygon) - if transform_y < 0: - convex_hull_polygon = flip_y(convex_hull_polygon) - node_list.append({"node": node, - "min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]], - }) + if len(node_list) < 2: + self._node_stack = node_list[:] + return - node_list = sorted(node_list, key = lambda d: d["min_coord"]) + # Copy the list + self._original_node_list = node_list[:] + + ## Initialise the hit map (pre-compute all hits between all objects) + self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list] + + # Check if we have to files that block eachother. If this is the case, there is no solution! + for a in range(0,len(node_list)): + for b in range(0,len(node_list)): + if a != b and self._hit_map[a][b] and self._hit_map[b][a]: + return + + # Sort the original list so that items that block the most other objects are at the beginning. + # This does not decrease the worst case running time, but should improve it in most cases. + sorted(node_list, key = cmp_to_key(self._calculateScore)) + + todo_node_list = [_ObjectOrder([], node_list)] + while len(todo_node_list) > 0: + current = todo_node_list.pop() + for node in current.todo: + # Check if the object can be placed with what we have and still allows for a solution in the future + if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo): + # We found a possible result. Create new todo & order list. + new_todo_list = current.todo[:] + new_todo_list.remove(node) + new_order = current.order[:] + [node] + if len(new_todo_list) == 0: + # We have no more nodes to check, so quit looking. + todo_node_list = None + self._node_stack = new_order + + return + todo_node_list.append(_ObjectOrder(new_order, new_todo_list)) + self._node_stack = [] #No result found! + + + # Check if first object can be printed before the provided list (using the hit map) + def _checkHitMultiple(self, node, other_nodes): + node_index = self._original_node_list.index(node) + for other_node in other_nodes: + other_node_index = self._original_node_list.index(other_node) + if self._hit_map[node_index][other_node_index]: + return True + return False + + def _checkBlockMultiple(self, node, other_nodes): + node_index = self._original_node_list.index(node) + for other_node in other_nodes: + other_node_index = self._original_node_list.index(other_node) + if self._hit_map[other_node_index][node_index] and node_index != other_node_index: + return True + return False + + ## Calculate score simply sums the number of other objects it 'blocks' + def _calculateScore(self, a, b): + score_a = sum(self._hit_map[self._original_node_list.index(a)]) + score_b = sum(self._hit_map[self._original_node_list.index(b)]) + return score_a - score_b + + # Checks if A can be printed before B + def _checkHit(self, a, b): + if a == b: + return False + + overlap = a.callDecoration("getConvexHullBoundary").intersectsPolygon(b.callDecoration("getConvexHullHeadFull")) + if overlap: + return True + else: + return False + + +## Internal object used to keep track of a possible order in which to print objects. +class _ObjectOrder(): + def __init__(self, order, todo): + """ + :param order: List of indexes in which to print objects, ordered by printing order. + :param todo: List of indexes which are not yet inserted into the order list. + """ + self.order = order + self.todo = todo - self._node_stack = [d["node"] for d in node_list] From c4c62cbba2228032d1a37d23f4834b0aba9e58bb Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 27 Sep 2019 14:15:31 +0200 Subject: [PATCH 02/31] Take nozzle offset into account in _getHeadAndFans() CURA-6785 --- cura/OneAtATimeIterator.py | 2 +- cura/Scene/ConvexHullDecorator.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index ab97534ff4..900eaf2273 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Scene.Iterator import Iterator diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index 2d8224eecc..bde7cde807 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -266,9 +266,13 @@ class ConvexHullDecorator(SceneNodeDecorator): return offset_hull def _getHeadAndFans(self) -> Polygon: - if self._global_stack: - return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32)) - return Polygon() + if not self._global_stack: + return Polygon() + + polygon = Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32)) + offset_x = self._getSettingProperty("machine_nozzle_offset_x", "value") + offset_y = self._getSettingProperty("machine_nozzle_offset_y", "value") + return polygon.translate(-offset_x, -offset_y) def _compute2DConvexHeadFull(self) -> Optional[Polygon]: convex_hull = self._compute2DConvexHull() From 69028bf27948e77fe9a4e50f5555fab41bc876af Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 30 Sep 2019 16:24:53 +0200 Subject: [PATCH 03/31] Remove unused import Contributes to issue CURA-6785. --- cura/OneAtATimeIterator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 900eaf2273..b66866a131 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -4,7 +4,6 @@ from UM.Scene.Iterator import Iterator from UM.Scene.SceneNode import SceneNode from functools import cmp_to_key -from UM.Application import Application ## Iterator that returns a list of nodes in the order that they need to be printed # If there is no solution an empty list is returned. From 7bf2fa3b43d1ffe30af1f6161d37ceb8ed09d905 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 30 Sep 2019 16:50:35 +0200 Subject: [PATCH 04/31] Add typing and documentation and remove unused code Contributes to issue CURA-6785. --- cura/OneAtATimeIterator.py | 48 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index b66866a131..a61ce492a9 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -1,6 +1,8 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import List + from UM.Scene.Iterator import Iterator from UM.Scene.SceneNode import SceneNode from functools import cmp_to_key @@ -9,12 +11,14 @@ from functools import cmp_to_key # If there is no solution an empty list is returned. # Take note that the list of nodes can have children (that may or may not contain mesh data) class OneAtATimeIterator(Iterator.Iterator): - def __init__(self, scene_node): - super().__init__(scene_node) # Call super to make multiple inheritence work. - self._hit_map = [[]] - self._original_node_list = [] + def __init__(self, scene_node) -> None: + super().__init__(scene_node) # Call super to make multiple inheritance work. + self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which. + self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions. - def _fillStack(self): + ## Fills the ``_node_stack`` with a list of scene nodes that need to be + # printed in order. + def _fillStack(self) -> None: node_list = [] for node in self._scene_node.getChildren(): if not issubclass(type(node), SceneNode): @@ -34,9 +38,9 @@ class OneAtATimeIterator(Iterator.Iterator): ## Initialise the hit map (pre-compute all hits between all objects) self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list] - # Check if we have to files that block eachother. If this is the case, there is no solution! - for a in range(0,len(node_list)): - for b in range(0,len(node_list)): + # Check if we have to files that block each other. If this is the case, there is no solution! + for a in range(0, len(node_list)): + for b in range(0, len(node_list)): if a != b and self._hit_map[a][b] and self._hit_map[b][a]: return @@ -56,16 +60,14 @@ class OneAtATimeIterator(Iterator.Iterator): new_order = current.order[:] + [node] if len(new_todo_list) == 0: # We have no more nodes to check, so quit looking. - todo_node_list = None self._node_stack = new_order - return todo_node_list.append(_ObjectOrder(new_order, new_todo_list)) self._node_stack = [] #No result found! # Check if first object can be printed before the provided list (using the hit map) - def _checkHitMultiple(self, node, other_nodes): + def _checkHitMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: node_index = self._original_node_list.index(node) for other_node in other_nodes: other_node_index = self._original_node_list.index(other_node) @@ -73,7 +75,10 @@ class OneAtATimeIterator(Iterator.Iterator): return True return False - def _checkBlockMultiple(self, node, other_nodes): + ## Check for a node whether it hits any of the other nodes. + # \param node The node to check whether it collides with the other nodes. + # \param other_nodes The nodes to check for collisions. + def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: node_index = self._original_node_list.index(node) for other_node in other_nodes: other_node_index = self._original_node_list.index(other_node) @@ -82,13 +87,13 @@ class OneAtATimeIterator(Iterator.Iterator): return False ## Calculate score simply sums the number of other objects it 'blocks' - def _calculateScore(self, a, b): + def _calculateScore(self, a: SceneNode, b: SceneNode) -> int: score_a = sum(self._hit_map[self._original_node_list.index(a)]) score_b = sum(self._hit_map[self._original_node_list.index(b)]) return score_a - score_b # Checks if A can be printed before B - def _checkHit(self, a, b): + def _checkHit(self, a: SceneNode, b: SceneNode) -> bool: if a == b: return False @@ -99,13 +104,12 @@ class OneAtATimeIterator(Iterator.Iterator): return False -## Internal object used to keep track of a possible order in which to print objects. -class _ObjectOrder(): - def __init__(self, order, todo): - """ - :param order: List of indexes in which to print objects, ordered by printing order. - :param todo: List of indexes which are not yet inserted into the order list. - """ +## Internal object used to keep track of a possible order in which to print objects. +class _ObjectOrder: + ## Creates the _ObjectOrder instance. + # \param order List of indices in which to print objects, ordered by printing + # order. + # \param todo: List of indices which are not yet inserted into the order list. + def __init__(self, order: List[SceneNode], todo: List[SceneNode]): self.order = order self.todo = todo - From 672fc58930052749c497862242f7871c888ac223 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 2 Oct 2019 12:59:30 +0200 Subject: [PATCH 05/31] Allow down to half the layer height for infill layer thickness This is possible because CuraEngine rounds these to the nearest layer thickness. So if it's more than half the layer height it gets rounded up and it's still properly one layer. Contributes to issue #6465. --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 3dbada96f5..122d0e3f5e 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1886,7 +1886,7 @@ "unit": "mm", "type": "float", "default_value": 0.1, - "minimum_value": "resolveOrValue('layer_height') if infill_line_distance > 0 else -999999", + "minimum_value": "resolveOrValue('layer_height') / 2 if infill_line_distance > 0 else -999999", "maximum_value_warning": "0.75 * machine_nozzle_size", "maximum_value": "resolveOrValue('layer_height') * (1.45 if spaghetti_infill_enabled else 8) if infill_line_distance > 0 else 999999", "value": "resolveOrValue('layer_height')", From f5f91c9c3a3086b5b576fc2e2907454c8e1d892d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 2 Oct 2019 13:06:27 +0200 Subject: [PATCH 06/31] Ensure that profiles are cleaned up if one of the set is incorrect --- cura/Settings/CuraContainerRegistry.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index c1a9ba9ead..3c93888b66 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -292,6 +292,7 @@ class CuraContainerRegistry(ContainerRegistry): profile_or_list.append(profile) # Import all profiles + profile_ids_added = [] # type: List[str] for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: # This is assumed to be the global profile @@ -312,11 +313,15 @@ class CuraContainerRegistry(ContainerRegistry): result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) if result is not None: + # Remove any profiles that did got added. + for profile_id in profile_ids_added: + self.removeContainer(profile_id) + return {"status": "error", "message": catalog.i18nc( "@info:status Don't translate the XML tag !", "Failed to import profile from {0}:", file_name) + " " + result} - + profile_ids_added.append(profile.getId()) return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} # This message is throw when the profile reader doesn't find any profile in the file From 95120300601c5a8391a3909c7291f95248d35052 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 2 Oct 2019 13:07:11 +0200 Subject: [PATCH 07/31] Check for adhesion area collisions in one-at-a-time ordering CURA-6785 --- cura/OneAtATimeIterator.py | 16 ++++++++++++++-- cura/Scene/ConvexHullDecorator.py | 13 ++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index a61ce492a9..4d420f6d05 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -92,12 +92,24 @@ class OneAtATimeIterator(Iterator.Iterator): score_b = sum(self._hit_map[self._original_node_list.index(b)]) return score_a - score_b - # Checks if A can be printed before B + ## Checks if A can be printed before B def _checkHit(self, a: SceneNode, b: SceneNode) -> bool: if a == b: return False - overlap = a.callDecoration("getConvexHullBoundary").intersectsPolygon(b.callDecoration("getConvexHullHeadFull")) + a_hit_hull = a.callDecoration("getConvexHullBoundary") + b_hit_hull = b.callDecoration("getConvexHullHeadFull") + overlap = a_hit_hull.intersectsPolygon(b_hit_hull) + + if overlap: + return True + + # Adhesion areas must never overlap, regardless of printing order + # This would cause over-extrusion + a_hit_hull = a.callDecoration("getAdhesionArea") + b_hit_hull = b.callDecoration("getAdhesionArea") + overlap = a_hit_hull.intersectsPolygon(b_hit_hull) + if overlap: return True else: diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index bde7cde807..c263726d07 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -76,7 +76,18 @@ class ConvexHullDecorator(SceneNodeDecorator): def __deepcopy__(self, memo): return ConvexHullDecorator() - ## Get the unmodified 2D projected convex hull of the node (if any) + + ## The polygon representing the 2D adhesion area. + # If no adhesion is used, the regular convex hull is returned + def getAdhesionArea(self) -> Optional[Polygon]: + if self._node is None: + return None + + hull = self._compute2DConvexHull() + return self._add2DAdhesionMargin(hull) + + + ## Get the unmodified 2D projected convex hull with 2D adhesion area of the node (if any) def getConvexHull(self) -> Optional[Polygon]: if self._node is None: return None From 1b1029a3e07816b46c9476c06048a63ccecb8a44 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 2 Oct 2019 13:13:32 +0200 Subject: [PATCH 08/31] Use 2 leading spaces for doxygen documentation CURA-6785 --- cura/OneAtATimeIterator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 4d420f6d05..b77e1f3982 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -66,7 +66,7 @@ class OneAtATimeIterator(Iterator.Iterator): self._node_stack = [] #No result found! - # Check if first object can be printed before the provided list (using the hit map) + # Check if first object can be printed before the provided list (using the hit map) def _checkHitMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: node_index = self._original_node_list.index(node) for other_node in other_nodes: @@ -92,7 +92,7 @@ class OneAtATimeIterator(Iterator.Iterator): score_b = sum(self._hit_map[self._original_node_list.index(b)]) return score_a - score_b - ## Checks if A can be printed before B + ## Checks if A can be printed before B def _checkHit(self, a: SceneNode, b: SceneNode) -> bool: if a == b: return False From 784396424c289a7a8bf65c67b30932722409d7f4 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Oct 2019 15:46:01 +0200 Subject: [PATCH 09/31] Get old diameter from old material instead of new candidate. part of CURA-6821 --- cura/Settings/MachineManager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index f822ec2ab8..0cd70970c8 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1357,11 +1357,7 @@ class MachineManager(QObject): # If we can keep the current material after the switch, try to do so. nozzle_node = ContainerTree.getInstance().machines[self._global_container_stack.definition.getId()].variants[current_nozzle_name] candidate_materials = nozzle_node.materials - old_approximate_material_diameter = None # type: Optional[float] - if candidate_materials: - candidate_material = list(candidate_materials.values())[0] - default_material_diameter = "2.85" - old_approximate_material_diameter = int(round(float(candidate_material.container.getMetaDataEntry("properties/diameter", default_material_diameter)))) + old_approximate_material_diameter = int(extruder.material.getMetaDataEntry("approximate_diameter", default = 3)) new_approximate_material_diameter = int(self._global_container_stack.extruderList[int(position_item)].getApproximateMaterialDiameter()) # Only switch to the old candidate material if the approximate material diameter of the extruder stays the From 5ec6b2fdf792cf6ad3a76450f56cce68c34efb0d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 2 Oct 2019 15:48:10 +0200 Subject: [PATCH 10/31] Fix typing CURA-6785 --- cura/Scene/ConvexHullDecorator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index c263726d07..72e95c9299 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -76,7 +76,6 @@ class ConvexHullDecorator(SceneNodeDecorator): def __deepcopy__(self, memo): return ConvexHullDecorator() - ## The polygon representing the 2D adhesion area. # If no adhesion is used, the regular convex hull is returned def getAdhesionArea(self) -> Optional[Polygon]: @@ -84,8 +83,10 @@ class ConvexHullDecorator(SceneNodeDecorator): return None hull = self._compute2DConvexHull() - return self._add2DAdhesionMargin(hull) + if hull is None: + return None + return self._add2DAdhesionMargin(hull) ## Get the unmodified 2D projected convex hull with 2D adhesion area of the node (if any) def getConvexHull(self) -> Optional[Polygon]: From c6850b4f73789dfee1eedeb0ad8d84f1ca946997 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 2 Oct 2019 16:39:50 +0200 Subject: [PATCH 11/31] Set right text color if menubutton is disbled CURA-6848 --- resources/qml/PrintSetupSelector/Custom/MenuButton.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/PrintSetupSelector/Custom/MenuButton.qml b/resources/qml/PrintSetupSelector/Custom/MenuButton.qml index acd8ed07bb..ffa6a68c9d 100644 --- a/resources/qml/PrintSetupSelector/Custom/MenuButton.qml +++ b/resources/qml/PrintSetupSelector/Custom/MenuButton.qml @@ -49,6 +49,6 @@ Button anchors.leftMargin: UM.Theme.getSize("wide_margin").width renderType: Text.NativeRendering font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") + color: button.enabled ? UM.Theme.getColor("text") :UM.Theme.getColor("text_inactive") } } \ No newline at end of file From ff3837bac5d4379ddada37eabaf7f98d9c4d1cbc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 2 Oct 2019 16:48:57 +0200 Subject: [PATCH 12/31] Only show "Custom Profile" header if there are custom profiles CURA-6844 --- .../qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml index c5a0df0bc5..efaa0f1dd0 100644 --- a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml +++ b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml @@ -134,7 +134,7 @@ Popup renderType: Text.NativeRendering height: visible ? contentHeight: 0 enabled: false - visible: profilesList.visibleChildren.length > 0 + visible: profilesList.visibleChildren.length > 1 anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width } @@ -156,7 +156,7 @@ Popup target: parent property: "height" value: parent.childrenRect.height - when: parent.visibleChildren.length > 0 + when: parent.visibleChildren.length > 1 } //Add all the custom profiles. From 8beeac7fca275a723e0846ff446a80f76ff8c393 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 2 Oct 2019 16:57:04 +0200 Subject: [PATCH 13/31] Make the radio button in the checkbar look consistent. The drake meme and CURA-6845 told me that it should look like that. --- resources/qml/RadioCheckbar.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/qml/RadioCheckbar.qml b/resources/qml/RadioCheckbar.qml index 727907254e..0ce84ad8ca 100644 --- a/resources/qml/RadioCheckbar.qml +++ b/resources/qml/RadioCheckbar.qml @@ -140,7 +140,6 @@ Item { anchors { - margins: 3 fill: parent } radius: Math.round(width / 2) From fba7a6ff33519abb5b12b1532615d42eeca880b1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 2 Oct 2019 17:01:21 +0200 Subject: [PATCH 14/31] Fix importing profiles CURA-6847 --- plugins/CuraProfileReader/CuraProfileReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py index fa8ca89442..d4e5d393b2 100644 --- a/plugins/CuraProfileReader/CuraProfileReader.py +++ b/plugins/CuraProfileReader/CuraProfileReader.py @@ -97,7 +97,7 @@ class CuraProfileReader(ProfileReader): if global_stack is None: return None - active_quality_definition = ContainerTree.getInstance().machines[global_stack.definition.container_id].quality_definition + active_quality_definition = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition if profile.getMetaDataEntry("definition") != active_quality_definition: profile.setMetaDataEntry("definition", active_quality_definition) return profile From a93fd982ddf9b786db5d4627d73c6adea0c21669 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 2 Oct 2019 15:21:38 +0200 Subject: [PATCH 15/31] Fix renaming custom profile CURA-6842 --- cura/Machines/MachineNode.py | 1 + cura/Machines/Models/QualityManagementModel.py | 16 ++++++++++++++-- cura/Machines/QualityChangesGroup.py | 11 +++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cura/Machines/MachineNode.py b/cura/Machines/MachineNode.py index d136151f44..83e8b053fc 100644 --- a/cura/Machines/MachineNode.py +++ b/cura/Machines/MachineNode.py @@ -142,6 +142,7 @@ class MachineNode(ContainerNode): parent = CuraApplication.getInstance()) elif groups_by_name[name].intent_category == "default": # Intent category should be stored as "default" if everything is default or as the intent if any of the extruder have an actual intent. groups_by_name[name].intent_category = quality_changes.get("intent_category", "default") + if "position" in quality_changes: # An extruder profile. groups_by_name[name].metadata_per_extruder[int(quality_changes["position"])] = quality_changes else: # Global profile. diff --git a/cura/Machines/Models/QualityManagementModel.py b/cura/Machines/Models/QualityManagementModel.py index 6789059cba..dcaa43283c 100644 --- a/cura/Machines/Models/QualityManagementModel.py +++ b/cura/Machines/Models/QualityManagementModel.py @@ -87,11 +87,23 @@ class QualityManagementModel(ListModel): application = cura.CuraApplication.CuraApplication.getInstance() container_registry = application.getContainerRegistry() new_name = container_registry.uniqueName(new_name) - global_container = cast(InstanceContainer, container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])[0]) - global_container.setName(new_name) + # CURA-6842 + # FIXME: setName() will trigger metaDataChanged signal that are connected with type Qt.AutoConnection. In this + # case, setName() will trigger direct connections which in turn causes the quality changes group and the models + # to update. Because multiple containers need to be renamed, and every time a container gets renamed, updates + # gets triggered and this results in partial updates. For example, if we rename the global quality changes + # container first, the rest of the system still thinks that I have selected "my_profile" instead of + # "my_new_profile", but an update already gets triggered, and the quality changes group that's selected will + # have no container for the global stack, because "my_profile" just got renamed to "my_new_profile". This results + # in crashes because the rest of the system assumes that all data in a QualityChangesGroup will be correct. + # + # Renaming the container for the global stack in the end seems to be ok, because the assumption is mostly based + # on the quality changes container for the global stack. for metadata in quality_changes_group.metadata_per_extruder.values(): extruder_container = cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0]) extruder_container.setName(new_name) + global_container = cast(InstanceContainer, container_registry.findContainers(id=quality_changes_group.metadata_for_global["id"])[0]) + global_container.setName(new_name) quality_changes_group.name = new_name diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index 4fb9706a0a..655060070b 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Optional -from PyQt5.QtCore import QObject, pyqtProperty +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal ## Data struct to group several quality changes instance containers together. @@ -22,7 +22,14 @@ class QualityChangesGroup(QObject): self.metadata_for_global = {} # type: Dict[str, Any] self.metadata_per_extruder = {} # type: Dict[int, Dict[str, Any]] - @pyqtProperty(str, constant = True) + nameChanged = pyqtSignal() + + def setName(self, name: str) -> None: + if self._name != name: + self._name = name + self.nameChanged.emit() + + @pyqtProperty(str, fset = setName, notify = nameChanged) def name(self) -> str: return self._name From 1def289bb9555a52c1c2fe5da5d20c635b8b46de Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 3 Oct 2019 09:51:45 +0200 Subject: [PATCH 16/31] Fix crash when importing profile that has different quality_definition CURA-6847 --- cura/Settings/CuraContainerRegistry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 84b714fdcf..557246ab1b 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -238,7 +238,7 @@ class CuraContainerRegistry(ContainerRegistry): # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... - profile_definition = container_tree.machines[machine_definition.getId()].quality_definition + profile_definition = machine_definition.getMetaDataEntry("quality_definition", "") expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition # And check if the profile_definition matches either one (showing error if not): From 2d59271a6936654e7cc88ad7406bcad637c6e524 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 3 Oct 2019 12:26:18 +0200 Subject: [PATCH 17/31] Remove hard-coded height --- .../qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml index efaa0f1dd0..f057701f60 100644 --- a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml +++ b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml @@ -238,9 +238,10 @@ Popup Cura.ContainerManager.clearUserContainers() } } + Rectangle { - height: 1 + height: UM.Theme.getSize("default_lining").width anchors.left: parent.left anchors.right: parent.right color: borderColor From ba805b9da3f4d26f7bb9dda45b6ab148dacba786 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 3 Oct 2019 12:26:56 +0200 Subject: [PATCH 18/31] Fix QML errors CURA-6849 --- resources/qml/Preferences/ProfileTab.qml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/resources/qml/Preferences/ProfileTab.qml b/resources/qml/Preferences/ProfileTab.qml index 12846cf99b..3c0c46ed72 100644 --- a/resources/qml/Preferences/ProfileTab.qml +++ b/resources/qml/Preferences/ProfileTab.qml @@ -38,14 +38,28 @@ Tab property var setting: qualitySettings.getItem(styleData.row) height: childrenRect.height width: (parent != null) ? parent.width : 0 - text: (styleData.value.substr(0,1) == "=") ? styleData.value : "" + text: + { + if (styleData.value === undefined) + { + return "" + } + return (styleData.value.substr(0,1) == "=") ? styleData.value : "" + } Label { anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.right: parent.right - text: (styleData.value.substr(0,1) == "=") ? catalog.i18nc("@info:status", "Calculated") : styleData.value + text: + { + if (styleData.value === undefined) + { + return "" + } + return (styleData.value.substr(0,1) == "=") ? catalog.i18nc("@info:status", "Calculated") : styleData.value + } font.strikeout: styleData.column == 1 && setting.user_value != "" && base.isQualityItemCurrentlyActivated font.italic: setting.profile_value_source == "quality_changes" || (setting.user_value != "" && base.isQualityItemCurrentlyActivated) opacity: font.strikeout ? 0.5 : 1 From 26b523f190b4c819720294aaaa77e8ef6b46d84d Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 3 Oct 2019 12:37:08 +0200 Subject: [PATCH 19/31] Fix quality profile menu size CURA-6838 --- .../qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml index f057701f60..dfd532e34c 100644 --- a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml +++ b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml @@ -261,7 +261,9 @@ Popup contentItem: Item { - width: manageProfilesButton.width + width: parent.width + height: childrenRect.height + Label { id: textLabel From 027768f151e60467414ef7bfc3bdb5549d6b054d Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 3 Oct 2019 12:56:20 +0200 Subject: [PATCH 20/31] Use machine def id as quality_definition fallback CURA-6847 --- cura/Settings/CuraContainerRegistry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 557246ab1b..6aadb9bc6f 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -238,7 +238,7 @@ class CuraContainerRegistry(ContainerRegistry): # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... - profile_definition = machine_definition.getMetaDataEntry("quality_definition", "") + profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition # And check if the profile_definition matches either one (showing error if not): From bd84c4d98d66f0039e26ff4bb9c95cd513d3a2d1 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 3 Oct 2019 14:18:49 +0200 Subject: [PATCH 21/31] Show quality and intent a custom profile is based on CURA-6846 --- cura/Settings/MachineManager.py | 24 +++++++++++++++++++ .../Custom/CustomPrintSetup.qml | 12 ++++++---- .../PrintSetupSelectorHeader.qml | 13 ++++++---- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 0cd70970c8..37789b23a1 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -629,6 +629,16 @@ class MachineManager(QObject): intent_category = category return intent_category + # Returns the human-readable name of the active intent category. If the intent category is "default", returns an + # empty string. + @pyqtProperty(str, notify = activeIntentChanged) + def activeIntentName(self) -> str: + intent_category = self.activeIntentCategory + if intent_category == "default": + intent_category = "" + intent_name = intent_category.capitalize() + return intent_name + # Provies a list of extruder positions that have a different intent from the active one. @pyqtProperty("QStringList", notify=activeIntentChanged) def extruderPositionsWithNonActiveIntent(self): @@ -1663,11 +1673,25 @@ class MachineManager(QObject): return global_container_stack.qualityChanges.getName() return global_container_stack.quality.getName() + @pyqtProperty(str, notify = activeQualityChanged) + def activeQualityName(self) -> str: + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if global_stack is None: + return empty_quality_container.getName() + return global_stack.quality.getName() + @pyqtProperty(bool, notify = activeQualityGroupChanged) def hasNotSupportedQuality(self) -> bool: global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() return (not global_container_stack is None) and global_container_stack.quality == empty_quality_container and global_container_stack.qualityChanges == empty_quality_changes_container + @pyqtProperty(bool, notify = activeQualityGroupChanged) + def isActiveQualityCustom(self) -> bool: + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if global_stack is None: + return False + return global_stack.qualityChanges != empty_quality_changes_container + def _updateUponMaterialMetadataChange(self) -> None: if self._global_container_stack is None: return diff --git a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml index c45a5fa8d7..33e2888fbb 100644 --- a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml +++ b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml @@ -88,14 +88,18 @@ Item function generateActiveQualityText() { + var result = Cura.MachineManager.activeQualityOrQualityChangesName - var result = "" - if(Cura.MachineManager.activeIntentCategory != "default") + // If this is a custom quality, add intent (if present) and quality it is based on + if (Cura.MachineManager.isActiveQualityCustom) { - result += Cura.MachineManager.activeIntentCategory + " - " + if (Cura.MachineManager.activeIntentName != "") + { + result += " - " + Cura.MachineManager.activeIntentName + } + result += " - " + Cura.MachineManager.activeQualityName } - result += Cura.MachineManager.activeQualityOrQualityChangesName if (Cura.MachineManager.isActiveQualityExperimental) { result += " (Experimental)" diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml index 5628867922..9340f64d89 100644 --- a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml +++ b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml @@ -20,13 +20,18 @@ RowLayout { if (Cura.MachineManager.activeStack) { - var text = "" - if(Cura.MachineManager.activeIntentCategory != "default") + var text = Cura.MachineManager.activeQualityOrQualityChangesName + + // If this is a custom quality, add intent (if present) and quality it is based on + if (Cura.MachineManager.isActiveQualityCustom) { - text += Cura.MachineManager.activeIntentCategory + " - " + if (Cura.MachineManager.activeIntentName != "") + { + text += " - " + Cura.MachineManager.activeIntentName + } + text += " - " + Cura.MachineManager.activeQualityName } - text += Cura.MachineManager.activeQualityOrQualityChangesName if (!Cura.MachineManager.hasNotSupportedQuality) { text += " - " + layerHeight.properties.value + "mm" From 877bb1efdbd4a649533cefaf33f790c894456209 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 3 Oct 2019 15:55:38 +0200 Subject: [PATCH 22/31] Diable local printer ListView buffer to force redraw CURA-6793 --- resources/qml/WelcomePages/AddLocalPrinterScrollView.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml b/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml index 6b074d2d8e..e4a7a98308 100644 --- a/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml +++ b/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml @@ -86,7 +86,11 @@ Item { id: machineList - cacheBuffer: 1000000 // Set a large cache to effectively just cache every list item. + // CURA-6793 + // Enabling the buffer seems to cause the blank items issue. When buffer is enabled, if the ListView's + // individual item has a dynamic change on its visibility, the ListView doesn't redraw itself. + // The default value of cacheBuffer is platform-dependent, so we explicitly disable it here. + cacheBuffer: 0 model: UM.DefinitionContainersModel { From 38ae0e75f2914bafcf2a326ebcc1624e291fcf05 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 3 Oct 2019 16:25:09 +0200 Subject: [PATCH 23/31] Fallback to fdmprinter if not has_machine_quality Contributes to issue CURA-6847. --- cura/Settings/CuraContainerRegistry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 6aadb9bc6f..bd5269e874 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -21,6 +21,7 @@ from UM.Message import Message from UM.Platform import Platform from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. from UM.Resources import Resources +from UM.Util import parseBool from cura.ReaderWriters.ProfileWriter import ProfileWriter from . import ExtruderStack @@ -238,7 +239,8 @@ class CuraContainerRegistry(ContainerRegistry): # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... - profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) + has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) + profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition # And check if the profile_definition matches either one (showing error if not): From f8951e4140488a0ffa2c09e1953388a83c582e53 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 4 Oct 2019 09:39:26 +0200 Subject: [PATCH 24/31] Add scrollbar to profile dropdown CURA-6843 --- .../Custom/QualitiesWithIntentMenu.qml | 267 +++++++++--------- 1 file changed, 138 insertions(+), 129 deletions(-) diff --git a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml index dfd532e34c..78925028a1 100644 --- a/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml +++ b/resources/qml/PrintSetupSelector/Custom/QualitiesWithIntentMenu.qml @@ -41,147 +41,156 @@ Popup contentItem: Column { // This repeater adds the intent labels - Repeater + ScrollView { - model: dataModel - delegate: Item - { - // We need to set it like that, otherwise we'd have to set the sub model with model: model.qualities - // Which obviously won't work due to naming conflicts. - property variant subItemModel: model.qualities + property real maximumHeight: screenScaleFactor * 400 - height: childrenRect.height - anchors - { - left: parent.left - right: parent.right - } + height: Math.min(contentHeight, maximumHeight) + clip: true - Label - { - id: headerLabel - text: model.name - renderType: Text.NativeRendering - height: visible ? contentHeight: 0 - enabled: false - visible: qualitiesList.visibleChildren.length > 0 - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - } - - Column - { - id: qualitiesList - anchors.top: headerLabel.bottom - anchors.left: parent.left - anchors.right: parent.right - - // We set it by means of a binding, since then we can use the when condition, which we need to - // prevent a binding loop. - Binding - { - target: parent - property: "height" - value: parent.childrenRect.height - when: parent.visibleChildren.length > 0 - } - - // Add the qualities that belong to the intent - Repeater - { - visible: false - model: subItemModel - MenuButton - { - id: button - - onClicked: Cura.IntentManager.selectIntent(model.intent_category, model.quality_type) - - width: parent.width - checkable: true - visible: model.available - text: model.name + " - " + model.layer_height + " mm" - checked: - { - if (Cura.MachineManager.hasCustomQuality) - { - // When user created profile is active, no quality tickbox should be active. - return false; - } - return Cura.MachineManager.activeQualityType == model.quality_type && Cura.MachineManager.activeIntentCategory == model.intent_category; - } - ButtonGroup.group: buttonGroup - } - } - } - } - } - - //Another "intent category" for custom profiles. - Item - { - height: childrenRect.height - anchors - { - left: parent.left - right: parent.right - } - - Label - { - id: customProfileHeader - text: catalog.i18nc("@label:header", "Custom profiles") - renderType: Text.NativeRendering - height: visible ? contentHeight: 0 - enabled: false - visible: profilesList.visibleChildren.length > 1 - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - } + ScrollBar.vertical.policy: height == maximumHeight ? ScrollBar.AlwaysOn: ScrollBar.AlwaysOff Column { - id: profilesList - anchors - { - top: customProfileHeader.bottom - left: parent.left - right: parent.right - } - - //We set it by means of a binding, since then we can use the - //"when" condition, which we need to prevent a binding loop. - Binding - { - target: parent - property: "height" - value: parent.childrenRect.height - when: parent.visibleChildren.length > 1 - } - - //Add all the custom profiles. + width: parent.width Repeater { - model: Cura.CustomQualityProfilesDropDownMenuModel - MenuButton + model: dataModel + delegate: Item { - onClicked: Cura.MachineManager.setQualityChangesGroup(model.quality_changes_group) + // We need to set it like that, otherwise we'd have to set the sub model with model: model.qualities + // Which obviously won't work due to naming conflicts. + property variant subItemModel: model.qualities - width: parent.width - checkable: true - visible: model.available - text: model.name - checked: + height: childrenRect.height + width: popup.contentWidth + + Label { - var active_quality_group = Cura.MachineManager.activeQualityChangesGroup - - if (active_quality_group != null) - { - return active_quality_group.name == model.quality_changes_group.name - } - return false + id: headerLabel + text: model.name + renderType: Text.NativeRendering + height: visible ? contentHeight: 0 + enabled: false + visible: qualitiesList.visibleChildren.length > 0 + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + } + + Column + { + id: qualitiesList + anchors.top: headerLabel.bottom + anchors.left: parent.left + anchors.right: parent.right + + // We set it by means of a binding, since then we can use the when condition, which we need to + // prevent a binding loop. + Binding + { + target: parent + property: "height" + value: parent.childrenRect.height + when: parent.visibleChildren.length > 0 + } + + // Add the qualities that belong to the intent + Repeater + { + visible: false + model: subItemModel + MenuButton + { + id: button + + onClicked: Cura.IntentManager.selectIntent(model.intent_category, model.quality_type) + + width: parent.width + checkable: true + visible: model.available + text: model.name + " - " + model.layer_height + " mm" + checked: + { + if (Cura.MachineManager.hasCustomQuality) + { + // When user created profile is active, no quality tickbox should be active. + return false; + } + return Cura.MachineManager.activeQualityType == model.quality_type && Cura.MachineManager.activeIntentCategory == model.intent_category; + } + ButtonGroup.group: buttonGroup + } + } + } + } + } + //Another "intent category" for custom profiles. + Item + { + height: childrenRect.height + anchors + { + left: parent.left + right: parent.right + } + + Label + { + id: customProfileHeader + text: catalog.i18nc("@label:header", "Custom profiles") + renderType: Text.NativeRendering + height: visible ? contentHeight: 0 + enabled: false + visible: profilesList.visibleChildren.length > 1 + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + } + + Column + { + id: profilesList + anchors + { + top: customProfileHeader.bottom + left: parent.left + right: parent.right + } + + //We set it by means of a binding, since then we can use the + //"when" condition, which we need to prevent a binding loop. + Binding + { + target: parent + property: "height" + value: parent.childrenRect.height + when: parent.visibleChildren.length > 1 + } + + //Add all the custom profiles. + Repeater + { + model: Cura.CustomQualityProfilesDropDownMenuModel + MenuButton + { + onClicked: Cura.MachineManager.setQualityChangesGroup(model.quality_changes_group) + + width: parent.width + checkable: true + visible: model.available + text: model.name + checked: + { + var active_quality_group = Cura.MachineManager.activeQualityChangesGroup + + if (active_quality_group != null) + { + return active_quality_group.name == model.quality_changes_group.name + } + return false + } + ButtonGroup.group: buttonGroup + } } - ButtonGroup.group: buttonGroup } } } From 61a1b61756e4d977719884091dd26b0c53c2de46 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 4 Oct 2019 10:27:35 +0200 Subject: [PATCH 25/31] Remove unnecessary activeQualityName property CURA-6846 --- cura/Settings/MachineManager.py | 8 -------- .../qml/PrintSetupSelector/Custom/CustomPrintSetup.qml | 2 +- .../qml/PrintSetupSelector/PrintSetupSelectorHeader.qml | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 37789b23a1..4518304680 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -656,7 +656,6 @@ class MachineManager(QObject): return result - ## Returns whether there is anything unsupported in the current set-up. # # The current set-up signifies the global stack and all extruder stacks, @@ -1673,13 +1672,6 @@ class MachineManager(QObject): return global_container_stack.qualityChanges.getName() return global_container_stack.quality.getName() - @pyqtProperty(str, notify = activeQualityChanged) - def activeQualityName(self) -> str: - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if global_stack is None: - return empty_quality_container.getName() - return global_stack.quality.getName() - @pyqtProperty(bool, notify = activeQualityGroupChanged) def hasNotSupportedQuality(self) -> bool: global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() diff --git a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml index 33e2888fbb..55ae33b134 100644 --- a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml +++ b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml @@ -97,7 +97,7 @@ Item { result += " - " + Cura.MachineManager.activeIntentName } - result += " - " + Cura.MachineManager.activeQualityName + result += " - " + Cura.MachineManager.activeQualityGroup.getName() } if (Cura.MachineManager.isActiveQualityExperimental) diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml index 9340f64d89..affe514bd8 100644 --- a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml +++ b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml @@ -29,7 +29,7 @@ RowLayout { text += " - " + Cura.MachineManager.activeIntentName } - text += " - " + Cura.MachineManager.activeQualityName + text += " - " + Cura.MachineManager.activeQualityGroup.getName() } if (!Cura.MachineManager.hasNotSupportedQuality) From c86f28cc9eea455b0c4d66998253f9a8243c24a8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 4 Oct 2019 10:33:13 +0200 Subject: [PATCH 26/31] Ignore disabled extruders when checking for intent warning CURA-6601 --- cura/Settings/MachineManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 37789b23a1..62f22431c0 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -650,6 +650,8 @@ class MachineManager(QObject): active_intent_category = self.activeIntentCategory result = [] for extruder in global_container_stack.extruderList: + if not extruder.isEnabled: + continue category = extruder.intent.getMetaDataEntry("intent_category", "default") if category != active_intent_category: result.append(str(int(extruder.getMetaDataEntry("position")) + 1)) @@ -1054,6 +1056,7 @@ class MachineManager(QObject): self.forceUpdateAllSettings() # Also trigger the build plate compatibility to update self.activeMaterialChanged.emit() + self.activeIntentChanged.emit() def _onMachineNameChanged(self) -> None: self.globalContainerChanged.emit() From 7233bdb2559407989c449e634a998ab2cbf24c5a Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 4 Oct 2019 10:36:57 +0200 Subject: [PATCH 27/31] Fix TestMachineNode: remove invalid property has_machine_materials --- tests/Machines/TestMachineNode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Machines/TestMachineNode.py b/tests/Machines/TestMachineNode.py index 4582f4c87d..270c2503e6 100644 --- a/tests/Machines/TestMachineNode.py +++ b/tests/Machines/TestMachineNode.py @@ -55,7 +55,6 @@ def test_metadataProperties(container_registry): # Check if each of the metadata entries got stored properly. assert not node.has_materials assert node.has_variants - assert node.has_machine_materials assert node.has_machine_quality assert node.quality_definition == metadata_dict["quality_definition"] assert node.exclude_materials == metadata_dict["exclude_materials"] From 5bd5875a20b7aa87695403d7e262a8c75b010bad Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 4 Oct 2019 10:39:02 +0200 Subject: [PATCH 28/31] Also add intent warning to custom print setup CURA-6601 --- .../PrintSetupSelector/Custom/CustomPrintSetup.qml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml index 55ae33b134..cae34dc5c5 100644 --- a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml +++ b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml @@ -7,7 +7,7 @@ import QtQuick.Controls 1.4 as OldControls import UM 1.3 as UM import Cura 1.6 as Cura - +import ".." Item { @@ -50,6 +50,18 @@ Item verticalAlignment: Text.AlignVCenter } + NoIntentIcon + { + affected_extruders: Cura.MachineManager.extruderPositionsWithNonActiveIntent + intent_type: Cura.MachineManager.activeIntentCategory + anchors.right: intentSelection.left + anchors.rightMargin: UM.Theme.getSize("narrow_margin").width + width: Math.round(profileLabel.height * 0.5) + anchors.verticalCenter: parent.verticalCenter + height: width + visible: affected_extruders.length + } + Button { id: intentSelection From e5c59b130894562be5cbea72ca8d9e42ea561721 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 4 Oct 2019 10:59:28 +0200 Subject: [PATCH 29/31] Fix TestVariantNode --- tests/Machines/TestVariantNode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Machines/TestVariantNode.py b/tests/Machines/TestVariantNode.py index e04c369762..954904908b 100644 --- a/tests/Machines/TestVariantNode.py +++ b/tests/Machines/TestVariantNode.py @@ -13,13 +13,13 @@ material_node_added_test_data = [({"type": "Not a material"}, ["material_1", "ma ({"type": "material", "base_file": "material_3"}, ["material_1", "material_2"]), # material_3 is on the "NOPE" list. ({"type": "material", "base_file": "material_4", "definition": "machine_3"}, ["material_1", "material_2"]), # Wrong machine ({"type": "material", "base_file": "material_4", "definition": "machine_1"}, ["material_1", "material_2"]), # No variant - ({"type": "material", "base_file": "material_4", "definition": "machine_1", "variant": "Variant Three"}, ["material_1", "material_2"]), # Wrong variant - ({"type": "material", "base_file": "material_4", "definition": "machine_1", "variant": "Variant One"}, ["material_1", "material_2", "material_4"]) + ({"type": "material", "base_file": "material_4", "definition": "machine_1", "variant_name": "Variant Three"}, ["material_1", "material_2"]), # Wrong variant + ({"type": "material", "base_file": "material_4", "definition": "machine_1", "variant_name": "Variant One"}, ["material_1", "material_2", "material_4"]) ] -material_node_update_test_data = [({"type": "material", "base_file": "material_1", "definition": "machine_1", "variant": "Variant One"}, ["material_1"], ["material_2"]), - ({"type": "material", "base_file": "material_1", "definition": "fdmprinter", "variant": "Variant One"}, [], ["material_2", "material_1"]), # Too generic - ({"type": "material", "base_file": "material_1", "definition": "machine_2", "variant": "Variant One"}, [], ["material_2", "material_1"]) # Wrong definition +material_node_update_test_data = [({"type": "material", "base_file": "material_1", "definition": "machine_1", "variant_name": "Variant One"}, ["material_1"], ["material_2"]), + ({"type": "material", "base_file": "material_1", "definition": "fdmprinter", "variant_name": "Variant One"}, [], ["material_2", "material_1"]), # Too generic + ({"type": "material", "base_file": "material_1", "definition": "machine_2", "variant_name": "Variant One"}, [], ["material_2", "material_1"]) # Wrong definition ] metadata_dict = {} From 618cbffaa7773fca8dc5eb7ddb2fd3002f85eea8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 4 Oct 2019 11:46:59 +0200 Subject: [PATCH 30/31] Apply screenScaleFactor to implicitWidth (and cleanup) --- resources/qml/LabelBar.qml | 2 +- .../Recommended/RecommendedQualityProfileSelector.qml | 3 ++- resources/qml/RadioCheckbar.qml | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/qml/LabelBar.qml b/resources/qml/LabelBar.qml index 9a870811ca..007c5f1f54 100644 --- a/resources/qml/LabelBar.qml +++ b/resources/qml/LabelBar.qml @@ -7,7 +7,7 @@ import QtQuick.Layouts 1.3 import UM 1.2 as UM -// The labelBar shows a set of labels that are evenly spaced from oneother. +// The labelBar shows a set of labels that are evenly spaced from one another. // The first item is aligned to the left, the last is aligned to the right. // It's intended to be used together with RadioCheckBar. As such, it needs // to know what the used itemSize is, so it can ensure the labels are aligned correctly. diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml index 99a1d25138..d1f7dd7de2 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 @@ -9,6 +9,7 @@ import QtQuick.Controls.Styles 1.4 import UM 1.2 as UM import Cura 1.6 as Cura import ".." + Item { id: qualityRow diff --git a/resources/qml/RadioCheckbar.qml b/resources/qml/RadioCheckbar.qml index 0ce84ad8ca..dfd9ca8628 100644 --- a/resources/qml/RadioCheckbar.qml +++ b/resources/qml/RadioCheckbar.qml @@ -19,7 +19,7 @@ Item property int barSize: UM.Theme.getSize("slider_groove_radius").height property var isCheckedFunction // Function that accepts the modelItem and returns if the item should be active. - implicitWidth: 200 + implicitWidth: 200 * screenScaleFactor implicitHeight: checkboxSize property var dataModel: null @@ -62,7 +62,7 @@ Item Layout.fillHeight: true // The last item of the repeater needs to be shorter, as we don't need another part to fit // the horizontal bar. The others should essentially not be limited. - Layout.maximumWidth: index + 1 === repeater.count ? activeComponent.width: 200000000 + Layout.maximumWidth: index + 1 === repeater.count ? activeComponent.width : 200000000 property bool isEnabled: model.available // The horizontal bar between the checkable options. From c528000268c7553b0d3d93176875ac34f74c9d76 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 4 Oct 2019 14:25:16 +0200 Subject: [PATCH 31/31] Add activeQualityDisplayName to remove code duplication CURA-6846 --- cura/Settings/MachineManager.py | 46 +++++++++++++++---- .../Custom/CustomPrintSetup.qml | 12 +---- .../PrintSetupSelectorHeader.qml | 12 +---- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 49405156e5..06f4e9be4e 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -123,6 +123,14 @@ class MachineManager(QObject): self.globalContainerChanged.connect(self.printerConnectedStatusChanged) self.outputDevicesChanged.connect(self.printerConnectedStatusChanged) + # For updating active quality display name + self.activeQualityChanged.connect(self.activeQualityDisplayNameChanged) + self.activeIntentChanged.connect(self.activeQualityDisplayNameChanged) + self.activeQualityGroupChanged.connect(self.activeQualityDisplayNameChanged) + self.activeQualityChangesGroupChanged.connect(self.activeQualityDisplayNameChanged) + + activeQualityDisplayNameChanged = pyqtSignal() + activeQualityGroupChanged = pyqtSignal() activeQualityChangesGroupChanged = pyqtSignal() @@ -629,16 +637,6 @@ class MachineManager(QObject): intent_category = category return intent_category - # Returns the human-readable name of the active intent category. If the intent category is "default", returns an - # empty string. - @pyqtProperty(str, notify = activeIntentChanged) - def activeIntentName(self) -> str: - intent_category = self.activeIntentCategory - if intent_category == "default": - intent_category = "" - intent_name = intent_category.capitalize() - return intent_name - # Provies a list of extruder positions that have a different intent from the active one. @pyqtProperty("QStringList", notify=activeIntentChanged) def extruderPositionsWithNonActiveIntent(self): @@ -1591,6 +1589,34 @@ class MachineManager(QObject): if not no_dialog and self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: self._application.discardOrKeepProfileChanges() + # The display name of currently active quality. + # This display name is: + # - For built-in qualities (quality/intent): the quality type name, such as "Fine", "Normal", etc. + # - For custom qualities: - - + # Examples: + # - "my_profile - Fine" (only based on a default quality, no intent involved) + # - "my_profile - Engineering - Fine" (based on an intent) + @pyqtProperty(str, notify = activeQualityDisplayNameChanged) + def activeQualityDisplayName(self) -> str: + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if global_stack is None: + return "" + + # Not a custom quality + display_name = self.activeQualityOrQualityChangesName + if global_stack.qualityChanges == empty_quality_changes_container: + return display_name + + # A custom quality + intent_category = self.activeIntentCategory + if intent_category != "default": + from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel + intent_display_name = IntentCategoryModel.name_translation.get(intent_category, catalog.i18nc("@label", "Unknown")) + display_name += " - {intent_name}".format(intent_name = intent_display_name) + + display_name += " - {quality_level_name}".format(quality_level_name = global_stack.quality.getName()) + return display_name + ## Change the intent category of the current printer. # # All extruders can change their profiles. If an intent profile is diff --git a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml index cae34dc5c5..a297b0a769 100644 --- a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml +++ b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml @@ -100,17 +100,7 @@ Item function generateActiveQualityText() { - var result = Cura.MachineManager.activeQualityOrQualityChangesName - - // If this is a custom quality, add intent (if present) and quality it is based on - if (Cura.MachineManager.isActiveQualityCustom) - { - if (Cura.MachineManager.activeIntentName != "") - { - result += " - " + Cura.MachineManager.activeIntentName - } - result += " - " + Cura.MachineManager.activeQualityGroup.getName() - } + var result = Cura.MachineManager.activeQualityDisplayName if (Cura.MachineManager.isActiveQualityExperimental) { diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml index affe514bd8..bb3a986929 100644 --- a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml +++ b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml @@ -20,17 +20,7 @@ RowLayout { if (Cura.MachineManager.activeStack) { - var text = Cura.MachineManager.activeQualityOrQualityChangesName - - // If this is a custom quality, add intent (if present) and quality it is based on - if (Cura.MachineManager.isActiveQualityCustom) - { - if (Cura.MachineManager.activeIntentName != "") - { - text += " - " + Cura.MachineManager.activeIntentName - } - text += " - " + Cura.MachineManager.activeQualityGroup.getName() - } + var text = Cura.MachineManager.activeQualityDisplayName if (!Cura.MachineManager.hasNotSupportedQuality) {