Merge branch '2.3' of github.com:Ultimaker/Cura into 2.3

This commit is contained in:
Tim Kuipers 2016-09-08 16:19:06 +02:00
commit 84fb59a0f3
34 changed files with 318 additions and 96 deletions

View File

@ -67,6 +67,9 @@ class BuildVolume(SceneNode):
self._disallowed_areas = [] self._disallowed_areas = []
self._disallowed_area_mesh = None self._disallowed_area_mesh = None
self._prime_tower_area = None
self._prime_tower_area_mesh = None
self.setCalculateBoundingBox(False) self.setCalculateBoundingBox(False)
self._volume_aabb = None self._volume_aabb = None
@ -82,6 +85,8 @@ class BuildVolume(SceneNode):
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged) ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
self._onActiveExtruderStackChanged() self._onActiveExtruderStackChanged()
self._has_errors = False
def setWidth(self, width): def setWidth(self, width):
if width: self._width = width if width: self._width = width
@ -110,6 +115,10 @@ class BuildVolume(SceneNode):
if self._disallowed_area_mesh: if self._disallowed_area_mesh:
renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9) renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
if self._prime_tower_area_mesh:
renderer.queueNode(self, mesh = self._prime_tower_area_mesh, shader = self._shader, transparent=True,
backface_cull=True, sort=-8)
return True return True
## Recalculates the build volume & disallowed areas. ## Recalculates the build volume & disallowed areas.
@ -184,6 +193,24 @@ class BuildVolume(SceneNode):
else: else:
self._disallowed_area_mesh = None self._disallowed_area_mesh = None
if self._prime_tower_area:
mb = MeshBuilder()
color = Color(1.0, 0.0, 0.0, 0.5)
points = self._prime_tower_area.getPoints()
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
for point in points:
new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
self._clamp(point[1], min_d, max_d))
mb.addFace(first, previous_point, new_point, color=color)
previous_point = new_point
self._prime_tower_area_mesh = mb.build()
else:
self._prime_tower_area_mesh = None
self._volume_aabb = AxisAlignedBox( self._volume_aabb = AxisAlignedBox(
minimum = Vector(min_w, min_h - 1.0, min_d), minimum = Vector(min_w, min_h - 1.0, min_d),
maximum = Vector(max_w, max_h - self._raft_thickness, max_d)) maximum = Vector(max_w, max_h - self._raft_thickness, max_d))
@ -291,24 +318,34 @@ class BuildVolume(SceneNode):
if rebuild_me: if rebuild_me:
self.rebuild() self.rebuild()
def hasErrors(self):
return self._has_errors
def _updateDisallowedAreas(self): def _updateDisallowedAreas(self):
if not self._global_container_stack: if not self._global_container_stack:
return return
self._has_errors = False # Reset.
disallowed_areas = copy.deepcopy( disallowed_areas = copy.deepcopy(
self._global_container_stack.getProperty("machine_disallowed_areas", "value")) self._global_container_stack.getProperty("machine_disallowed_areas", "value"))
areas = [] areas = []
machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_width = self._global_container_stack.getProperty("machine_width", "value")
machine_depth = self._global_container_stack.getProperty("machine_depth", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
self._prime_tower_area = None
# Add prime tower location as disallowed area. # Add prime tower location as disallowed area.
if self._global_container_stack.getProperty("prime_tower_enable", "value") == True: if self._global_container_stack.getProperty("prime_tower_enable", "value") == True:
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value") prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2 prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2
prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2 prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2
disallowed_areas.append([ '''disallowed_areas.append([
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y],
[prime_tower_x - prime_tower_size, prime_tower_y],
])'''
self._prime_tower_area = Polygon([
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size], [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y - prime_tower_size], [prime_tower_x, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y], [prime_tower_x, prime_tower_y],
@ -344,6 +381,9 @@ class BuildVolume(SceneNode):
areas.append(poly) areas.append(poly)
if self._prime_tower_area:
self._prime_tower_area = self._prime_tower_area.getMinkowskiHull(Polygon(approximatedCircleVertices(bed_adhesion_size)))
# Add the skirt areas around the borders of the build plate. # Add the skirt areas around the borders of the build plate.
if bed_adhesion_size > 0: if bed_adhesion_size > 0:
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2 half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
@ -377,6 +417,19 @@ class BuildVolume(SceneNode):
[half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size] [half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size]
], numpy.float32))) ], numpy.float32)))
# Check if the prime tower area intersects with any of the other areas.
# If this is the case, keep the polygon seperate, so it can be drawn in red.
# If not, add it back to disallowed area's, so it's rendered as normal.
collision = False
if self._prime_tower_area:
for area in areas:
if self._prime_tower_area.intersectsPolygon(area) is not None:
collision = True
break
if not collision:
areas.append(self._prime_tower_area)
self._prime_tower_area = None
self._has_errors = collision
self._disallowed_areas = areas self._disallowed_areas = areas
## Convenience function to calculate the size of the bed adhesion in directions x, y. ## Convenience function to calculate the size of the bed adhesion in directions x, y.

View File

@ -322,6 +322,7 @@ class CuraApplication(QtApplication):
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name) path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
if path: if path:
instance.setPath(path)
with SaveFile(path, "wt", -1, "utf-8") as f: with SaveFile(path, "wt", -1, "utf-8") as f:
f.write(data) f.write(data)
@ -346,6 +347,7 @@ class CuraApplication(QtApplication):
elif stack_type == "extruder_train": elif stack_type == "extruder_train":
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name) path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
if path: if path:
stack.setPath(path)
with SaveFile(path, "wt", -1, "utf-8") as f: with SaveFile(path, "wt", -1, "utf-8") as f:
f.write(data) f.write(data)
@ -693,12 +695,12 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"): if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
nodes.append(node) nodes.append(node)
if nodes: if nodes:
op = GroupedOperation() op = GroupedOperation()
for node in nodes: for node in nodes:
# Ensure that the object is above the build platform
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
op.addOperation(SetTransformOperation(node, Vector(0, node.getWorldPosition().y - node.getBoundingBox().bottom, 0))) op.addOperation(SetTransformOperation(node, Vector(0, node.getWorldPosition().y - node.getBoundingBox().bottom, 0)))
op.push() op.push()
@ -719,13 +721,15 @@ class CuraApplication(QtApplication):
if nodes: if nodes:
op = GroupedOperation() op = GroupedOperation()
for node in nodes: for node in nodes:
# Ensure that the object is above the build platform # Ensure that the object is above the build platform
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
center_y = 0
op.addOperation(SetTransformOperation(node, Vector(0, node.getMeshData().getCenterPosition().y, 0), Quaternion(), Vector(1, 1, 1))) if node.callDecoration("isGroup"):
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else:
center_y = node.getMeshData().getCenterPosition().y
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
op.push() op.push()
## Reload all mesh data on the screen from file. ## Reload all mesh data on the screen from file.

View File

@ -16,7 +16,7 @@ class LayerPolygon:
MoveRetractionType = 9 MoveRetractionType = 9
SupportInterfaceType = 10 SupportInterfaceType = 10
__jump_map = numpy.logical_or( numpy.arange(11) == NoneType, numpy.arange(11) >= MoveRetractionType ) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(11) == NoneType, numpy.arange(11) == MoveCombingType), numpy.arange(11) == MoveRetractionType)
def __init__(self, mesh, extruder, line_types, data, line_widths): def __init__(self, mesh, extruder, line_types, data, line_widths):
self._mesh = mesh self._mesh = mesh
@ -42,7 +42,7 @@ class LayerPolygon:
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool) self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
self._build_cache_line_mesh_mask = None self._build_cache_line_mesh_mask = None
self._build_cache_needed_points = None self._build_cache_needed_points = None

View File

@ -15,6 +15,9 @@ from cura.ConvexHullDecorator import ConvexHullDecorator
from . import PlatformPhysicsOperation from . import PlatformPhysicsOperation
from . import ZOffsetDecorator from . import ZOffsetDecorator
import random # used for list shuffling
class PlatformPhysics: class PlatformPhysics:
def __init__(self, controller, volume): def __init__(self, controller, volume):
super().__init__() super().__init__()
@ -48,7 +51,12 @@ class PlatformPhysics:
# same direction. # same direction.
transformed_nodes = [] transformed_nodes = []
for node in BreadthFirstIterator(root): group_nodes = []
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
nodes = list(BreadthFirstIterator(root))
random.shuffle(nodes)
for node in nodes:
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
continue continue
@ -69,6 +77,9 @@ class PlatformPhysics:
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
node._outside_buildarea = True node._outside_buildarea = True
if node.callDecoration("isGroup"):
group_nodes.append(node) # Keep list of affected group_nodes
# Move it downwards if bottom is above platform # Move it downwards if bottom is above platform
move_vector = Vector() move_vector = Vector()
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
@ -102,7 +113,6 @@ class PlatformPhysics:
continue # Other node is already moving, wait for next pass. continue # Other node is already moving, wait for next pass.
overlap = (0, 0) # Start loop with no overlap overlap = (0, 0) # Start loop with no overlap
move_vector = move_vector.set(x=overlap[0] * self._move_factor, z=overlap[1] * self._move_factor)
current_overlap_checks = 0 current_overlap_checks = 0
# Continue to check the overlap until we no longer find one. # Continue to check the overlap until we no longer find one.
while overlap and current_overlap_checks < self._max_overlap_checks: while overlap and current_overlap_checks < self._max_overlap_checks:
@ -144,7 +154,6 @@ class PlatformPhysics:
overlap = convex_hull.intersectsPolygon(area) overlap = convex_hull.intersectsPolygon(area)
if overlap is None: if overlap is None:
continue continue
node._outside_buildarea = True node._outside_buildarea = True
if not Vector.Null.equals(move_vector, epsilon=1e-5): if not Vector.Null.equals(move_vector, epsilon=1e-5):
@ -152,6 +161,12 @@ class PlatformPhysics:
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
op.push() op.push()
# Group nodes should override the _outside_buildarea property of their children.
for group_node in group_nodes:
for child_node in group_node.getAllChildren():
child_node._outside_buildarea = group_node._outside_buildarea
def _onToolOperationStarted(self, tool): def _onToolOperationStarted(self, tool):
self._enabled = False self._enabled = False

View File

@ -167,6 +167,19 @@ class ContainerManager(QObject):
return True return True
@pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id, entry_name):
containers = self._container_registry.findContainers(None, id=container_id)
if not containers:
UM.Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
return False
result = containers[0].getMetaDataEntry(entry_name)
if result:
return result
else:
return ""
## Set a metadata entry of the specified container. ## Set a metadata entry of the specified container.
# #
# This will set the specified entry of the container's metadata to the specified # This will set the specified entry of the container's metadata to the specified
@ -254,6 +267,10 @@ class ContainerManager(QObject):
return True return True
return False return False
@pyqtSlot(str, result = str)
def makeUniqueName(self, original_name):
return self._container_registry.uniqueName(original_name)
## Get a list of string that can be used as name filters for a Qt File Dialog ## Get a list of string that can be used as name filters for a Qt File Dialog
# #
# This will go through the list of available container types and generate a list of strings # This will go through the list of available container types and generate a list of strings
@ -573,6 +590,28 @@ class ContainerManager(QObject):
return new_name return new_name
@pyqtSlot(str, result = str)
def duplicateMaterial(self, material_id):
containers = self._container_registry.findInstanceContainers(id=material_id)
if not containers:
UM.Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
return ""
# Ensure all settings are saved.
UM.Application.getInstance().saveSettings()
# Create a new ID & container to hold the data.
new_id = self._container_registry.uniqueName(material_id)
container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer
duplicated_container = container_type(new_id)
# Instead of duplicating we load the data from the basefile again.
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
# are also correctly created.
with open(containers[0].getPath(), encoding="utf-8") as f:
duplicated_container.deserialize(f.read())
self._container_registry.addContainer(duplicated_container)
# Factory function, used by QML # Factory function, used by QML
@staticmethod @staticmethod
def createContainerManager(engine, js_engine): def createContainerManager(engine, js_engine):

View File

@ -71,7 +71,7 @@ class ThreeMFReader(MeshReader):
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0)) rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
# TODO: We currently do not check for normals and simply recalculate them. # TODO: We currently do not check for normals and simply recalculate them.
mesh_builder.calculateNormals() mesh_builder.calculateNormals(flip = True)
mesh_builder.setFileName(file_name) mesh_builder.setFileName(file_name)
node.setMeshData(mesh_builder.build().getTransformed(rotation)) node.setMeshData(mesh_builder.build().getTransformed(rotation))
node.setSelectable(True) node.setSelectable(True)

View File

@ -78,6 +78,10 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.SettingError) self.setResult(StartJobResult.SettingError)
return return
if Application.getInstance().getBuildVolume().hasErrors():
self.setResult(StartJobResult.SettingError)
return
# Don't slice if there is a per object setting with an error value. # Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable(): if type(node) is not SceneNode or not node.isSelectable():

View File

@ -1,8 +1,8 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher. # Cura is released under the terms of the AGPLv3 or higher.
import configparser
import os.path from UM import PluginRegistry
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make. from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.ProfileReader import ProfileReader from cura.ProfileReader import ProfileReader
@ -27,22 +27,76 @@ class CuraProfileReader(ProfileReader):
# returned. # returned.
def read(self, file_name): def read(self, file_name):
try: try:
archive = zipfile.ZipFile(file_name, "r") with zipfile.ZipFile(file_name, "r") as archive:
except Exception: results = []
# zipfile doesn't give proper exceptions, so we can only catch broad ones for profile_id in archive.namelist():
with archive.open(profile_id) as f:
serialized = f.read()
profile = self._loadProfile(serialized.decode("utf-8"), profile_id)
if profile is not None:
results.append(profile)
return results
except zipfile.BadZipFile:
# It must be an older profile from Cura 2.1.
with open(file_name, encoding="utf-8") as fhandle:
serialized = fhandle.read()
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)]
## Convert a profile from an old Cura to this Cura if needed.
#
# \param serialized \type{str} The profile data to convert in the serialized on-disk format.
# \param profile_id \type{str} The name of the profile.
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
def _upgradeProfile(self, serialized, profile_id):
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized)
if not "general" in parser:
Logger.log("w", "Missing required section 'general'.")
return None
if not "version" in parser["general"]:
Logger.log("w", "Missing required 'version' property")
return None
version = int(parser["general"]["version"])
if InstanceContainer.Version != version:
name = parser["general"]["name"]
return self._upgradeProfileVersion(serialized, name, version)
else:
return [(serialized, profile_id)]
## Load a profile from a serialized string.
#
# \param serialized \type{str} The profile data to read.
# \param profile_id \type{str} The name of the profile.
# \return \type{InstanceContainer|None}
def _loadProfile(self, serialized, profile_id):
# Create an empty profile.
profile = InstanceContainer(profile_id)
profile.addMetaDataEntry("type", "quality_changes")
try:
profile.deserialize(serialized)
except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
Logger.log("e", "Error while trying to parse profile: %s", str(e))
return None
return profile
## Upgrade a serialized profile to the current profile format.
#
# \param serialized \type{str} The profile data to convert.
# \param profile_id \type{str} The name of the profile.
# \param source_version \type{int} The profile version of 'serialized'.
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
def _upgradeProfileVersion(self, serialized, profile_id, source_version):
converter_plugins = PluginRegistry.getInstance().getAllMetaData(filter={"version_upgrade": {} }, active_only=True)
source_format = ("profile", source_version)
profile_convert_funcs = [plugin["version_upgrade"][source_format][2] for plugin in converter_plugins
if source_format in plugin["version_upgrade"] and plugin["version_upgrade"][source_format][1] == InstanceContainer.Version]
if not profile_convert_funcs:
return [] return []
results = []
for profile_id in archive.namelist(): filenames, outputs = profile_convert_funcs[0](serialized, profile_id)
# Create an empty profile. return list(zip(outputs, filenames))
profile = InstanceContainer(profile_id)
profile.addMetaDataEntry("type", "quality_changes")
serialized = ""
with archive.open(profile_id) as f:
serialized = f.read()
try:
profile.deserialize(serialized.decode("utf-8") )
except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
Logger.log("e", "Error while trying to parse profile: %s", str(e))
continue
results.append(profile)
return results

View File

@ -66,7 +66,10 @@ class GCodeWriter(MeshWriter):
## Create a new container with container 2 as base and container 1 written over it. ## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2): def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
flat_container = InstanceContainer(instance_container2.getName()) flat_container = InstanceContainer(instance_container2.getName())
flat_container.setDefinition(instance_container2.getDefinition()) if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition())
else:
flat_container.setDefinition(instance_container2.getDefinition())
flat_container.setMetaData(instance_container2.getMetaData()) flat_container.setMetaData(instance_container2.getMetaData())
for key in instance_container2.getAllKeys(): for key in instance_container2.getAllKeys():

View File

@ -70,7 +70,8 @@
"magic_spiralize": "spiralize", "magic_spiralize": "spiralize",
"prime_tower_enable": "wipe_tower", "prime_tower_enable": "wipe_tower",
"prime_tower_size": "math.sqrt(float(wipe_tower_volume) / float(layer_height))", "prime_tower_size": "math.sqrt(float(wipe_tower_volume) / float(layer_height))",
"ooze_shield_enabled": "ooze_shield" "ooze_shield_enabled": "ooze_shield",
"skin_overlap": "fill_overlap"
}, },
"defaults": { "defaults": {

View File

@ -69,7 +69,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0] stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else: else:
stack = UM.Application.getInstance().getGlobalContainerStack() stack = UM.Application.getInstance().getGlobalContainerStack()
new_instance.setProperty("value", stack.getProperty(item, "value")) new_instance.setProperty("value", stack.getRawProperty(item, "value"))
new_instance.resetState() # Ensure that the state is not seen as a user state. new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance) settings.addInstance(new_instance)
visibility_changed = True visibility_changed = True

View File

@ -7,6 +7,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Application import Application from UM.Application import Application
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from cura.Settings.ExtruderManager import ExtruderManager
## This tool allows the user to add & change settings per node in the scene. ## This tool allows the user to add & change settings per node in the scene.
@ -71,11 +72,20 @@ class PerObjectSettingsTool(Tool):
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1 self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
# Ensure that all extruder data is reset
if not self._multi_extrusion: if not self._multi_extrusion:
# Ensure that all extruder data is reset default_stack_id = global_container_stack.getId()
root_node = Application.getInstance().getController().getScene().getRoot() else:
for node in DepthFirstIterator(root_node): default_stack = ExtruderManager.getInstance().getExtruderStack(0)
node.callDecoration("setActiveExtruder", global_container_stack.getId()) if default_stack:
default_stack_id = default_stack.getId()
else: default_stack_id = global_container_stack.getId()
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
node.callDecoration("setActiveExtruder", default_stack_id)
self._updateEnabled() self._updateEnabled()
def _updateEnabled(self): def _updateEnabled(self):

View File

@ -49,7 +49,7 @@ class RemovableDrivePlugin(OutputDevicePlugin):
message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName())) message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName()))
message.show() message.show()
else: else:
message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName())) message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(device.getName()))
message.show() message.show()
return result return result

View File

@ -65,7 +65,7 @@ class SliceInfo(Extension):
Preferences.getInstance().addPreference("info/asked_send_slice_info", False) Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info"): if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False) self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in preferences"), lifetime = 0, dismissable = False)
self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "") self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered) self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show() self.send_slice_info_message.show()

View File

@ -13,6 +13,7 @@ from UM.Settings.Validator import ValidatorState
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
import cura.Settings import cura.Settings
from cura.Settings.ExtruderManager import ExtruderManager
import math import math
@ -45,8 +46,18 @@ class SolidView(View):
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
if multi_extrusion:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
if not support_angle_stack:
support_angle_stack = global_container_stack
else:
support_angle_stack = global_container_stack
if Preferences.getInstance().getValue("view/show_overhang"): if Preferences.getInstance().getValue("view/show_overhang"):
angle = global_container_stack.getProperty("support_angle", "value") angle = support_angle_stack.getProperty("support_angle", "value")
# Make sure the overhang angle is valid before passing it to the shader # Make sure the overhang angle is valid before passing it to the shader
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None) # Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]: if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]:
@ -56,7 +67,6 @@ class SolidView(View):
else: else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer): if not node.render(renderer):

View File

@ -140,7 +140,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# \param gcode_list List with gcode (strings). # \param gcode_list List with gcode (strings).
def printGCode(self, gcode_list): def printGCode(self, gcode_list):
if self._progress or self._connection_state != ConnectionState.connected: if self._progress or self._connection_state != ConnectionState.connected:
self._error_message = Message(catalog.i18nc("@info:status", "Printer is busy or not connected. Unable to start a new job.")) self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer is busy or not connected."))
self._error_message.show() self._error_message.show()
Logger.log("d", "Printer is busy or not connected, aborting print") Logger.log("d", "Printer is busy or not connected, aborting print")
self.writeError.emit(self) self.writeError.emit(self)

View File

@ -105,9 +105,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
@pyqtSlot(str) @pyqtSlot(str)
def updateAllFirmware(self, file_name): def updateAllFirmware(self, file_name):
file_name = file_name.replace("file://", "") # File dialogs prepend the path with file://, which we don't need / want if file_name.startswith("file://"):
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices: if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info", "Cannot update firmware, there were no connected printers found.")).show() Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected.")).show()
return return
for printer_connection in self._usb_output_devices: for printer_connection in self._usb_output_devices:

View File

@ -31,7 +31,7 @@ class UMOUpgradeSelection(MachineAction):
if variant: if variant:
if variant.getId() == "empty_variant": if variant.getId() == "empty_variant":
variant_index = global_container_stack.getContainerIndex(variant) variant_index = global_container_stack.getContainerIndex(variant)
self._createVariant(global_container_stack, variant_index) variant = self._createVariant(global_container_stack, variant_index)
variant.setProperty("machine_heated_bed", "value", heated_bed) variant.setProperty("machine_heated_bed", "value", heated_bed)
self.heatedBedChanged.emit() self.heatedBedChanged.emit()
@ -42,3 +42,4 @@ class UMOUpgradeSelection(MachineAction):
new_variant.setDefinition(global_container_stack.getBottom()) new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant) UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
global_container_stack.replaceContainer(variant_index, new_variant) global_container_stack.replaceContainer(variant_index, new_variant)
return new_variant

View File

@ -279,6 +279,8 @@ class VersionUpgrade21to22(VersionUpgrade):
elif key in _setting_name_translations: elif key in _setting_name_translations:
del settings[key] del settings[key]
settings[_setting_name_translations[key]] = value settings[_setting_name_translations[key]] = value
if "infill_overlap" in settings: # New setting, added in 2.3
settings["skin_overlap"] = settings["infill_overlap"]
return settings return settings
## Translates a setting name for the change from Cura 2.1 to 2.2. ## Translates a setting name for the change from Cura 2.1 to 2.2.

View File

@ -10,6 +10,7 @@
"manufacturer": "Ultimaker", "manufacturer": "Ultimaker",
"file_formats": "text/x-gcode;application/x-stl-ascii;application/x-stl-binary;application/x-wavefront-obj;application/x3g", "file_formats": "text/x-gcode;application/x-stl-ascii;application/x-stl-binary;application/x-wavefront-obj;application/x3g",
"visible": false, "visible": false,
"has_materials": true,
"preferred_material": "*generic_pla*", "preferred_material": "*generic_pla*",
"preferred_quality": "*normal*", "preferred_quality": "*normal*",
"machine_extruder_trains": "machine_extruder_trains":
@ -403,7 +404,7 @@
"description": "The maximum speed of the filament.", "description": "The maximum speed of the filament.",
"unit": "mm/s", "unit": "mm/s",
"type": "float", "type": "float",
"default_value": 25, "default_value": 299792458000,
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false, "settable_per_extruder": false,
"settable_per_meshgroup": false "settable_per_meshgroup": false
@ -1161,7 +1162,7 @@
"default_value": 25, "default_value": 25,
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "machine_max_feedrate_e", "maximum_value": "machine_max_feedrate_e",
"maximum_value_warning": "100", "maximum_value_warning": "25",
"enabled": "retraction_enable", "enabled": "retraction_enable",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true, "settable_per_extruder": true,
@ -1174,7 +1175,7 @@
"default_value": 25, "default_value": 25,
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "machine_max_feedrate_e", "maximum_value": "machine_max_feedrate_e",
"maximum_value_warning": "100", "maximum_value_warning": "25",
"enabled": "retraction_enable", "enabled": "retraction_enable",
"value": "retraction_speed", "value": "retraction_speed",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -1188,7 +1189,7 @@
"default_value": 25, "default_value": 25,
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "machine_max_feedrate_e", "maximum_value": "machine_max_feedrate_e",
"maximum_value_warning": "100", "maximum_value_warning": "25",
"enabled": "retraction_enable", "enabled": "retraction_enable",
"value": "retraction_speed", "value": "retraction_speed",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -3207,6 +3208,7 @@
"type": "category", "type": "category",
"icon": "category_dual", "icon": "category_dual",
"description": "Settings used for printing with multiple extruders.", "description": "Settings used for printing with multiple extruders.",
"enabled": "machine_extruder_count > 1",
"children": "children":
{ {
"adhesion_extruder_nr": "adhesion_extruder_nr":
@ -3284,6 +3286,7 @@
"default_value": 15, "default_value": 15,
"value": "15 if prime_tower_enable else 0", "value": "15 if prime_tower_enable else 0",
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)",
"maximum_value_warning": "20", "maximum_value_warning": "20",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false "settable_per_extruder": false

View File

@ -9,6 +9,9 @@
"visible": false "visible": false
}, },
"overrides": { "overrides": {
"machine_max_feedrate_e": {
"default_value": 45
},
"material_print_temperature": { "material_print_temperature": {
"minimum_value": "0" "minimum_value": "0"
}, },

View File

@ -14,6 +14,7 @@
"platform": "ultimaker2_platform.obj", "platform": "ultimaker2_platform.obj",
"platform_texture": "Ultimaker2backplate.png", "platform_texture": "Ultimaker2backplate.png",
"platform_offset": [9, 0, 0], "platform_offset": [9, 0, 0],
"has_materials": false,
"supported_actions":["UpgradeFirmware"] "supported_actions":["UpgradeFirmware"]
}, },
"overrides": { "overrides": {

View File

@ -78,7 +78,7 @@
"default_value": 185 "default_value": 185
}, },
"prime_tower_position_y": { "prime_tower_position_y": {
"default_value": 175 "default_value": 160
} }
} }
} }

View File

@ -14,11 +14,10 @@ Generic Nylon profile. Serves as an example file, data in this file is not corre
<color_code>#3DF266</color_code> <color_code>#3DF266</color_code>
</metadata> </metadata>
<properties> <properties>
<density>1.19</density> <density>1.14</density>
<diameter>2.85</diameter> <diameter>2.85</diameter>
</properties> </properties>
<settings> <settings>
<setting key="hardware compatible">no</setting><!-- This material is not supported on most printers due to high temperatures -->
<setting key="print temperature">250</setting> <setting key="print temperature">250</setting>
<setting key="heated bed temperature">60</setting> <setting key="heated bed temperature">60</setting>
<setting key="standby temperature">175</setting> <setting key="standby temperature">175</setting>

View File

@ -14,7 +14,7 @@ Generic PC profile. Serves as an example file, data in this file is not correct.
<color_code>#F29030</color_code> <color_code>#F29030</color_code>
</metadata> </metadata>
<properties> <properties>
<density>1.18</density> <density>1.19</density>
<diameter>2.85</diameter> <diameter>2.85</diameter>
</properties> </properties>
<settings> <settings>

View File

@ -14,7 +14,7 @@ Generic TPU 95A profile. Serves as an example file, data in this file is not cor
<color_code>#B22744</color_code> <color_code>#B22744</color_code>
</metadata> </metadata>
<properties> <properties>
<density>1.19</density> <density>1.22</density>
<diameter>2.85</diameter> <diameter>2.85</diameter>
</properties> </properties>
<settings> <settings>

View File

@ -464,12 +464,11 @@ UM.MainWindow
target: Cura.Actions.addProfile target: Cura.Actions.addProfile
onTriggered: onTriggered:
{ {
Cura.ContainerManager.createQualityChanges(null);
preferences.setPage(4); preferences.setPage(4);
preferences.show(); preferences.show();
// Show the renameDialog after a very short delay so the preference page has time to initiate // Create a new profile after a very short delay so the preference page has time to initiate
showProfileNameDialogTimer.start(); createProfileTimer.start();
} }
} }
@ -516,11 +515,11 @@ UM.MainWindow
Timer Timer
{ {
id: showProfileNameDialogTimer id: createProfileTimer
repeat: false repeat: false
interval: 1 interval: 1
onTriggered: preferences.getCurrentItem().showProfileNameDialog() onTriggered: preferences.getCurrentItem().createProfile()
} }
// BlurSettings is a way to force the focus away from any of the setting items. // BlurSettings is a way to force the focus away from any of the setting items.

View File

@ -115,7 +115,7 @@ TabView
width: base.secondColumnWidth; width: base.secondColumnWidth;
value: properties.density; value: properties.density;
decimals: 2 decimals: 2
suffix: "g/cm" suffix: "g/cm³"
stepSize: 0.01 stepSize: 0.01
readOnly: !base.editingEnabled; readOnly: !base.editingEnabled;
@ -128,7 +128,7 @@ TabView
width: base.secondColumnWidth; width: base.secondColumnWidth;
value: properties.diameter; value: properties.diameter;
decimals: 2 decimals: 2
suffix: "mm³" suffix: "mm"
stepSize: 0.01 stepSize: 0.01
readOnly: !base.editingEnabled; readOnly: !base.editingEnabled;

View File

@ -129,30 +129,24 @@ UM.ManagementPage
enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId
onClicked: Cura.MachineManager.setActiveMaterial(base.currentItem.id) onClicked: Cura.MachineManager.setActiveMaterial(base.currentItem.id)
}, },
/* // apparently visible does not work on OS X // apparently visible does not work on OS X
Button /*Button
{ {
text: catalog.i18nc("@action:button", "Duplicate"); text: catalog.i18nc("@action:button", "Duplicate");
iconName: "list-add"; iconName: "list-add";
enabled: base.currentItem != null enabled: base.currentItem != null
onClicked: onClicked:
{ {
var material_id = Cura.ContainerManager.duplicateContainer(base.currentItem.id) var base_file = Cura.ContainerManager.getContainerMetaDataEntry(base.currentItem.id, "base_file")
// We need to copy the base container instead of the specific variant.
var material_id = base_file == "" ? Cura.ContainerManager.duplicateMaterial(base.currentItem.id): Cura.ContainerManager.duplicateMaterial(base_file)
if(material_id == "") if(material_id == "")
{ {
return return
} }
if(Cura.MachineManager.filterQualityByMachine)
{
var quality_id = Cura.ContainerManager.duplicateContainer(Cura.MachineManager.activeQualityId)
Cura.ContainerManager.setContainerMetaDataEntry(quality_id, "material", material_id)
Cura.MachineManager.setActiveQuality(quality_id)
}
Cura.MachineManager.setActiveMaterial(material_id) Cura.MachineManager.setActiveMaterial(material_id)
} }
visible: false;
}, },
*/ */
Button Button

View File

@ -84,7 +84,7 @@ UM.ManagementPage
onClicked: onClicked:
{ {
newNameDialog.object = base.currentItem != null ? base.currentItem.name : ""; newNameDialog.object = base.currentItem != null ? Cura.ContainerManager.makeUniqueName(base.currentItem.name) : "";
newNameDialog.open(); newNameDialog.open();
newNameDialog.selectText(); newNameDialog.selectText();
} }
@ -100,7 +100,7 @@ UM.ManagementPage
onClicked: onClicked:
{ {
newDuplicateNameDialog.object = base.currentItem.name; newDuplicateNameDialog.object = Cura.ContainerManager.makeUniqueName(base.currentItem.name);
newDuplicateNameDialog.open(); newDuplicateNameDialog.open();
newDuplicateNameDialog.selectText(); newDuplicateNameDialog.selectText();
} }
@ -141,11 +141,12 @@ UM.ManagementPage
scrollviewCaption: catalog.i18nc("@label %1 is printer name","Printer: %1").arg(Cura.MachineManager.activeMachineName) scrollviewCaption: catalog.i18nc("@label %1 is printer name","Printer: %1").arg(Cura.MachineManager.activeMachineName)
signal showProfileNameDialog() signal createProfile()
onShowProfileNameDialog: onCreateProfile:
{ {
renameDialog.open(); newNameDialog.object = base.currentItem != null ? Cura.ContainerManager.makeUniqueName(base.currentItem.name) : "";
renameDialog.selectText(); newNameDialog.open();
newNameDialog.selectText();
} }
signal selectContainer(string name) signal selectContainer(string name)
@ -267,6 +268,7 @@ UM.ManagementPage
UM.RenameDialog UM.RenameDialog
{ {
title: catalog.i18nc("@title:window", "Rename Profile")
id: renameDialog; id: renameDialog;
object: base.currentItem != null ? base.currentItem.name : "" object: base.currentItem != null ? base.currentItem.name : ""
onAccepted: onAccepted:
@ -279,6 +281,7 @@ UM.ManagementPage
// Dialog to request a name when creating a new profile // Dialog to request a name when creating a new profile
UM.RenameDialog UM.RenameDialog
{ {
title: catalog.i18nc("@title:window", "Create Profile")
id: newNameDialog; id: newNameDialog;
object: "<new name>"; object: "<new name>";
onAccepted: onAccepted:
@ -292,6 +295,7 @@ UM.ManagementPage
// Dialog to request a name when duplicating a new profile // Dialog to request a name when duplicating a new profile
UM.RenameDialog UM.RenameDialog
{ {
title: catalog.i18nc("@title:window", "Duplicate Profile")
id: newDuplicateNameDialog; id: newDuplicateNameDialog;
object: "<new name>"; object: "<new name>";
onAccepted: onAccepted:

View File

@ -138,7 +138,7 @@ Item {
{ {
id: linkedSettingIcon; id: linkedSettingIcon;
visible: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId && !definition.settable_per_extruder && base.showLinkedSettingIcon visible: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId && (!definition.settable_per_extruder || definition.global_inherits_stack != "-1") && base.showLinkedSettingIcon
height: parent.height; height: parent.height;
width: height; width: height;

View File

@ -40,7 +40,7 @@ ScrollView
id: delegate id: delegate
width: UM.Theme.getSize("sidebar").width; width: UM.Theme.getSize("sidebar").width;
height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : 0 height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing
Behavior on height { NumberAnimation { duration: 100 } } Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0 opacity: provider.properties.enabled == "True" ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } } Behavior on opacity { NumberAnimation { duration: 100 } }

View File

@ -199,18 +199,32 @@ QtObject {
property Component progressbar: Component{ property Component progressbar: Component{
ProgressBarStyle { ProgressBarStyle {
background:Rectangle { background: Rectangle {
implicitWidth: Theme.getSize("message").width - (Theme.getSize("default_margin").width * 2) implicitWidth: Theme.getSize("message").width - (Theme.getSize("default_margin").width * 2)
implicitHeight: Theme.getSize("progressbar").height implicitHeight: Theme.getSize("progressbar").height
radius: Theme.getSize("progressbar_radius").width radius: Theme.getSize("progressbar_radius").width
color: Theme.getColor("progressbar_background") color: control.hasOwnProperty("backgroundColor") ? control.backgroundColor : Theme.getColor("progressbar_background")
} }
progress: Rectangle { progress: Rectangle {
color: control.indeterminate ? "transparent" : Theme.getColor("progressbar_control") color:
{
if(control.indeterminate)
{
return "transparent";
}
else if(control.hasOwnProperty("controlColor"))
{
return control.controlColor;
}
else
{
return Theme.getColor("progressbar_control");
}
}
radius: Theme.getSize("progressbar_radius").width radius: Theme.getSize("progressbar_radius").width
Rectangle{ Rectangle{
radius: Theme.getSize("progressbar_radius").width radius: Theme.getSize("progressbar_radius").width
color: Theme.getColor("progressbar_control") color: control.hasOwnProperty("controlColor") ? control.controlColor : Theme.getColor("progressbar_control")
width: Theme.getSize("progressbar_control").width width: Theme.getSize("progressbar_control").width
height: Theme.getSize("progressbar_control").height height: Theme.getSize("progressbar_control").height
visible: control.indeterminate visible: control.indeterminate

View File

@ -159,9 +159,17 @@
"tooltip": [12, 169, 227, 255], "tooltip": [12, 169, 227, 255],
"tooltip_text": [255, 255, 255, 255], "tooltip_text": [255, 255, 255, 255],
"message_background": [255, 255, 255, 255], "message_background": [24, 41, 77, 255],
"message_text": [32, 166, 219, 255], "message_text": [255, 255, 255, 255],
"message_dismiss": [127, 127, 127, 255], "message_border": [24, 41, 77, 255],
"message_button": [255, 255, 255, 255],
"message_button_hover": [12, 169, 227, 255],
"message_button_active": [32, 166, 219, 255],
"message_button_text": [24, 41, 77, 255],
"message_button_text_hover": [255, 255, 255, 255],
"message_button_text_active": [255, 255, 255, 255],
"message_progressbar_background": [255, 255, 255, 255],
"message_progressbar_control": [12, 169, 227, 255],
"tool_panel_background": [255, 255, 255, 255], "tool_panel_background": [255, 255, 255, 255],