Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/cura
diff options
context:
space:
mode:
authorGhostkeeper <rubend@tutanota.com>2022-02-21 13:52:28 +0300
committerGhostkeeper <rubend@tutanota.com>2022-02-21 13:52:28 +0300
commitc7d7dd11d143c804d0c1b3ff67fc6f89ed5171e6 (patch)
treedb5f78d4f88e85e01c408c03d3298c95cd14ebb9 /cura
parent28924a1c87032e29306d0fca067bb39abc2accf2 (diff)
parent614fe6fd4f0b6370b731c11d3cf5395f788b9ad4 (diff)
Merge branch 'master' into PyQt6_upgrade
Conflicts: cura/PlatformPhysics.py -> Removed shapely on master, while QTimer import got updated to Qt6. plugins/Toolbox -> Entire folder is deleted in master, but it was updated to Qt6 here. This can all be removed.
Diffstat (limited to 'cura')
-rw-r--r--cura/Arranging/Nest2DArrange.py2
-rwxr-xr-xcura/BuildVolume.py43
-rw-r--r--cura/CrashHandler.py9
-rwxr-xr-xcura/CuraApplication.py10
-rw-r--r--cura/CuraPackageManager.py44
-rw-r--r--cura/Machines/Models/MaterialManagementModel.py2
-rwxr-xr-xcura/PlatformPhysics.py9
-rwxr-xr-xcura/Settings/ExtruderManager.py32
-rw-r--r--cura/UltimakerCloud/CloudMaterialSync.py2
9 files changed, 108 insertions, 45 deletions
diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py
index dad67ba161..43d4d7f8a8 100644
--- a/cura/Arranging/Nest2DArrange.py
+++ b/cura/Arranging/Nest2DArrange.py
@@ -40,7 +40,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
machine_width = build_volume.getWidth()
machine_depth = build_volume.getDepth()
- build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor)
+ build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor))
if fixed_nodes is None:
fixed_nodes = []
diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py
index 372d6152fc..dd798ac7e7 100755
--- a/cura/BuildVolume.py
+++ b/cura/BuildVolume.py
@@ -72,7 +72,7 @@ class BuildVolume(SceneNode):
self._origin_mesh = None # type: Optional[MeshData]
self._origin_line_length = 20
- self._origin_line_width = 1.5
+ self._origin_line_width = 1
self._enabled = False
self._grid_mesh = None # type: Optional[MeshData]
@@ -601,6 +601,7 @@ class BuildVolume(SceneNode):
if self._adhesion_type == "raft":
self._raft_thickness = (
self._global_container_stack.getProperty("raft_base_thickness", "value") +
+ self._global_container_stack.getProperty("raft_interface_layers", "value") *
self._global_container_stack.getProperty("raft_interface_thickness", "value") +
self._global_container_stack.getProperty("raft_surface_layers", "value") *
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
@@ -848,10 +849,10 @@ class BuildVolume(SceneNode):
"""
result = {}
- adhesion_extruder = None #type: ExtruderStack
+ skirt_brim_extruder: ExtruderStack = None
for extruder in used_extruders:
- if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value")):
- adhesion_extruder = extruder
+ if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")):
+ skirt_brim_extruder = extruder
result[extruder.getId()] = []
# Currently, the only normally printed object is the prime tower.
@@ -865,11 +866,11 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2
- if adhesion_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
+ if skirt_brim_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
brim_size = (
- adhesion_extruder.getProperty("brim_line_count", "value") *
- adhesion_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
- adhesion_extruder.getProperty("initial_layer_line_width_factor", "value")
+ skirt_brim_extruder.getProperty("brim_line_count", "value") *
+ skirt_brim_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
+ skirt_brim_extruder.getProperty("initial_layer_line_width_factor", "value")
)
prime_tower_x -= brim_size
prime_tower_y += brim_size
@@ -1100,18 +1101,18 @@ class BuildVolume(SceneNode):
# with the adhesion extruder, but it also prints one extra line by all other extruders. As such, the
# setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
# the value is.
- adhesion_extruder = self._global_container_stack.getProperty("adhesion_extruder_nr", "value")
+ skirt_brim_extruder_nr = self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")
try:
- adhesion_stack = self._global_container_stack.extruderList[int(adhesion_extruder)]
+ skirt_brim_stack = self._global_container_stack.extruderList[int(skirt_brim_extruder_nr)]
except IndexError:
- Logger.warning(f"Couldn't find extruder with index '{adhesion_extruder}', defaulting to 0 instead.")
- adhesion_stack = self._global_container_stack.extruderList[0]
- skirt_brim_line_width = adhesion_stack.getProperty("skirt_brim_line_width", "value")
+ Logger.warning(f"Couldn't find extruder with index '{skirt_brim_extruder_nr}', defaulting to 0 instead.")
+ skirt_brim_stack = self._global_container_stack.extruderList[0]
+ skirt_brim_line_width = skirt_brim_stack.getProperty("skirt_brim_line_width", "value")
- initial_layer_line_width_factor = adhesion_stack.getProperty("initial_layer_line_width_factor", "value")
+ initial_layer_line_width_factor = skirt_brim_stack.getProperty("initial_layer_line_width_factor", "value")
# Use brim width if brim is enabled OR the prime tower has a brim.
if adhesion_type == "brim":
- brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
+ brim_line_count = skirt_brim_stack.getProperty("brim_line_count", "value")
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders:
@@ -1120,8 +1121,8 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the brim with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "skirt":
- skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
- skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
+ skirt_distance = skirt_brim_stack.getProperty("skirt_gap", "value")
+ skirt_line_count = skirt_brim_stack.getProperty("skirt_line_count", "value")
bed_adhesion_size = skirt_distance + (
skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
@@ -1132,7 +1133,7 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the skirt with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "raft":
- bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
+ bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value") # Should refer to the raft extruder if set.
elif adhesion_type == "none":
bed_adhesion_size = 0
else:
@@ -1214,13 +1215,13 @@ class BuildVolume(SceneNode):
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
- _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
+ _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_layers", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
- _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
- _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
+ _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
+ _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr"]
_material_size_settings = ["material_shrinkage_percentage", "material_shrinkage_percentage_xy", "material_shrinkage_percentage_z"]
_disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings + _material_size_settings
diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py
index f5ce13e04b..bda9568666 100644
--- a/cura/CrashHandler.py
+++ b/cura/CrashHandler.py
@@ -15,7 +15,7 @@ from typing import cast, Any
try:
from sentry_sdk.hub import Hub
from sentry_sdk.utils import event_from_exception
- from sentry_sdk import configure_scope
+ from sentry_sdk import configure_scope, add_breadcrumb
with_sentry_sdk = True
except ImportError:
with_sentry_sdk = False
@@ -424,6 +424,13 @@ class CrashHandler:
if with_sentry_sdk:
try:
hub = Hub.current
+ if not Logger.getLoggers():
+ # No loggers have been loaded yet, so we don't have any breadcrumbs :(
+ # So add them manually so we at least have some info...
+ add_breadcrumb(level = "info", message = "SentryLogging was not initialised yet")
+ for log_type, line in Logger.getUnloggedLines():
+ add_breadcrumb(message=line)
+
event, hint = event_from_exception((self.exception_type, self.value, self.traceback))
hub.capture_event(event, hint=hint)
hub.flush()
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 6eea5eaefe..7183789de3 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -494,7 +494,7 @@ class CuraApplication(QtApplication):
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
"FileLogger", #You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", #Cura crashes without this one.
- "Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
+ "Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"PrepareStage", #Cura is useless without this one since you can't load models.
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
"MonitorStage", #Major part of Cura's functionality.
@@ -573,6 +573,10 @@ class CuraApplication(QtApplication):
preferences.addPreference("general/accepted_user_agreement", False)
+ preferences.addPreference("cura/market_place_show_plugin_banner", True)
+ preferences.addPreference("cura/market_place_show_material_banner", True)
+ preferences.addPreference("cura/market_place_show_manage_packages_banner", True)
+
for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
"dialog_profile_path",
@@ -777,10 +781,14 @@ class CuraApplication(QtApplication):
lib_suffixes = {""}
for suffix in lib_suffixes:
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
+
if not hasattr(sys, "frozen"):
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
self._plugin_registry.preloaded_plugins.append("ConsoleLogger")
+ # Since it's possible to get crashes in code before the sentrylogger is loaded, we want to start this plugin
+ # as quickly as possible, as we might get unsolvable crash reports without it.
+ self._plugin_registry.preloaded_plugins.append("SentryLogger")
self._plugin_registry.loadPlugins()
if self.getBackend() is None:
diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py
index 212993e19b..17d6832ac6 100644
--- a/cura/CuraPackageManager.py
+++ b/cura/CuraPackageManager.py
@@ -1,13 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-from typing import List, Tuple, TYPE_CHECKING, Optional
+from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
-from cura.CuraApplication import CuraApplication #To find some resource types.
+from cura.CuraApplication import CuraApplication # To find some resource types.
from cura.Settings.GlobalStack import GlobalStack
-from UM.PackageManager import PackageManager #The class we're extending.
-from UM.Resources import Resources #To find storage paths for some resource types.
+from UM.PackageManager import PackageManager # The class we're extending.
+from UM.Resources import Resources # To find storage paths for some resource types.
+from UM.i18n import i18nCatalog
+catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from UM.Qt.QtApplication import QtApplication
@@ -17,6 +19,31 @@ if TYPE_CHECKING:
class CuraPackageManager(PackageManager):
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(application, parent)
+ self._local_packages: Optional[List[Dict[str, Any]]] = None
+ self._local_packages_ids: Optional[Set[str]] = None
+ self.installedPackagesChanged.connect(self._updateLocalPackages)
+
+ def _updateLocalPackages(self) -> None:
+ self._local_packages = self.getAllLocalPackages()
+ self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages)
+
+ @property
+ def local_packages(self) -> List[Dict[str, Any]]:
+ """locally installed packages, lazy execution"""
+ if self._local_packages is None:
+ self._updateLocalPackages()
+ # _updateLocalPackages always results in a list of packages, not None.
+ # It's guaranteed to be a list now.
+ return cast(List[Dict[str, Any]], self._local_packages)
+
+ @property
+ def local_packages_ids(self) -> Set[str]:
+ """locally installed packages, lazy execution"""
+ if self._local_packages_ids is None:
+ self._updateLocalPackages()
+ # _updateLocalPackages always results in a list of packages, not None.
+ # It's guaranteed to be a list now.
+ return cast(Set[str], self._local_packages_ids)
def initialize(self) -> None:
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
@@ -47,3 +74,12 @@ class CuraPackageManager(PackageManager):
machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
return machine_with_materials, machine_with_qualities
+
+ def getAllLocalPackages(self) -> List[Dict[str, Any]]:
+ """ Returns an unordered list of all the package_info of installed, to be installed, or bundled packages"""
+ packages: List[Dict[str, Any]] = []
+
+ for packages_to_add in self.getAllInstalledPackagesInfo().values():
+ packages.extend(packages_to_add)
+
+ return packages
diff --git a/cura/Machines/Models/MaterialManagementModel.py b/cura/Machines/Models/MaterialManagementModel.py
index ab904573c2..53d0cca0a2 100644
--- a/cura/Machines/Models/MaterialManagementModel.py
+++ b/cura/Machines/Models/MaterialManagementModel.py
@@ -60,7 +60,7 @@ class MaterialManagementModel(QObject):
sync_materials_message.addAction(
"sync",
- name = catalog.i18nc("@action:button", "Sync materials with printers"),
+ name = catalog.i18nc("@action:button", "Sync materials"),
icon = "",
description = "Sync your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py
index 1cd5c1844c..e054528c42 100755
--- a/cura/PlatformPhysics.py
+++ b/cura/PlatformPhysics.py
@@ -1,8 +1,7 @@
-# Copyright (c) 2020 Ultimaker B.V.
+# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QTimer
-from shapely.errors import TopologicalError # To capture errors if Shapely messes up.
from UM.Application import Application
from UM.Logger import Logger
@@ -138,11 +137,7 @@ class PlatformPhysics:
own_convex_hull = node.callDecoration("getConvexHull")
other_convex_hull = other_node.callDecoration("getConvexHull")
if own_convex_hull and other_convex_hull:
- try:
- overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
- except TopologicalError as e: # Can happen if the convex hull is degenerate?
- Logger.warning("Got a topological error when calculating convex hull intersection: {err}".format(err = str(e)))
- overlap = False
+ overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
if overlap: # Moving ensured that overlap was still there. Try anew!
temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z = move_vector.z + overlap[1] * self._move_factor)
diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py
index 34ba96e280..07c074254f 100755
--- a/cura/Settings/ExtruderManager.py
+++ b/cura/Settings/ExtruderManager.py
@@ -259,11 +259,21 @@ class ExtruderManager(QObject):
if support_roof_enabled:
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
- # The platform adhesion extruder. Not used if using none.
- if global_stack.getProperty("adhesion_type", "value") != "none" or (
- global_stack.getProperty("prime_tower_brim_enable", "value") and
- global_stack.getProperty("adhesion_type", "value") != 'raft'):
- extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
+ # The platform adhesion extruders.
+ used_adhesion_extruders = set()
+ adhesion_type = global_stack.getProperty("adhesion_type", "value")
+ if adhesion_type == "skirt" and (global_stack.getProperty("skirt_line_count", "value") > 0 or global_stack.getProperty("skirt_brim_minimal_length", "value") > 0):
+ used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a skirt.
+ if (adhesion_type == "brim" or global_stack.getProperty("prime_tower_brim_enable", "value")) and (global_stack.getProperty("brim_line_count", "value") > 0 or global_stack.getProperty("skirt_brim_minimal_length", "value") > 0):
+ used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a brim or prime tower brim.
+ if adhesion_type == "raft":
+ used_adhesion_extruders.add("raft_base_extruder_nr")
+ if global_stack.getProperty("raft_interface_layers", "value") > 0:
+ used_adhesion_extruders.add("raft_interface_extruder_nr")
+ if global_stack.getProperty("raft_surface_layers", "value") > 0:
+ used_adhesion_extruders.add("raft_surface_extruder_nr")
+ for extruder_setting in used_adhesion_extruders:
+ extruder_str_nr = str(global_stack.getProperty(extruder_setting, "value"))
if extruder_str_nr == "-1":
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
if extruder_str_nr in self.extruderIds:
@@ -286,8 +296,11 @@ class ExtruderManager(QObject):
global_stack = application.getGlobalContainerStack()
# Starts with the adhesion extruder.
- if global_stack.getProperty("adhesion_type", "value") != "none":
- return global_stack.getProperty("adhesion_extruder_nr", "value")
+ adhesion_type = global_stack.getProperty("adhesion_type", "value")
+ if adhesion_type in {"skirt", "brim"}:
+ return global_stack.getProperty("skirt_brim_extruder_nr", "value")
+ if adhesion_type == "raft":
+ return global_stack.getProperty("raft_base_extruder_nr", "value")
# No adhesion? Well maybe there is still support brim.
if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_structure", "value") == "tree") and global_stack.getProperty("support_brim_enable", "value"):
@@ -422,7 +435,10 @@ class ExtruderManager(QObject):
Logger.log("w", "Could not find the variant %s", active_variant_name)
return True
active_variant_node = machine_node.variants[active_variant_name]
- active_material_node = active_variant_node.materials[extruder_stack.material.getMetaDataEntry("base_file")]
+ try:
+ active_material_node = active_variant_node.materials[extruder_stack.material.getMetaDataEntry("base_file")]
+ except KeyError: # The material in this stack is not a supported material (e.g. wrong filament diameter, as loaded from a project file).
+ return False
active_material_node_qualities = active_material_node.qualities
if not active_material_node_qualities:
diff --git a/cura/UltimakerCloud/CloudMaterialSync.py b/cura/UltimakerCloud/CloudMaterialSync.py
index 9b3af4a1b3..e2eb50f97e 100644
--- a/cura/UltimakerCloud/CloudMaterialSync.py
+++ b/cura/UltimakerCloud/CloudMaterialSync.py
@@ -71,7 +71,7 @@ class CloudMaterialSync(QObject):
sync_materials_message.addAction(
"sync",
- name = catalog.i18nc("@action:button", "Sync materials with printers"),
+ name = catalog.i18nc("@action:button", "Sync materials"),
icon = "",
description = "Sync your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT