1# Copyright (c) 2020 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4import os
5import sys
6import time
7from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
8
9import numpy
10from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
11from PyQt5.QtGui import QColor, QIcon
12from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
13from PyQt5.QtWidgets import QMessageBox
14
15import UM.Util
16import cura.Settings.cura_empty_instance_containers
17from UM.Application import Application
18from UM.Decorators import override
19from UM.FlameProfiler import pyqtSlot
20from UM.Logger import Logger
21from UM.Math.AxisAlignedBox import AxisAlignedBox
22from UM.Math.Matrix import Matrix
23from UM.Math.Quaternion import Quaternion
24from UM.Math.Vector import Vector
25from UM.Mesh.ReadMeshJob import ReadMeshJob
26from UM.Message import Message
27from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
28from UM.Operations.GroupedOperation import GroupedOperation
29from UM.Operations.SetTransformOperation import SetTransformOperation
30from UM.Platform import Platform
31from UM.PluginError import PluginNotFoundError
32from UM.Preferences import Preferences
33from UM.Qt.QtApplication import QtApplication  # The class we're inheriting from.
34from UM.Resources import Resources
35from UM.Scene.Camera import Camera
36from UM.Scene.GroupDecorator import GroupDecorator
37from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
38from UM.Scene.SceneNode import SceneNode
39from UM.Scene.SceneNodeSettings import SceneNodeSettings
40from UM.Scene.Selection import Selection
41from UM.Scene.ToolHandle import ToolHandle
42from UM.Settings.ContainerRegistry import ContainerRegistry
43from UM.Settings.InstanceContainer import InstanceContainer
44from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
45from UM.Settings.SettingFunction import SettingFunction
46from UM.Settings.Validator import Validator
47from UM.View.SelectionPass import SelectionPass  # For typing.
48from UM.Workspace.WorkspaceReader import WorkspaceReader
49from UM.i18n import i18nCatalog
50from cura import ApplicationMetadata
51from cura.API import CuraAPI
52from cura.API.Account import Account
53from cura.Arranging.Arrange import Arrange
54from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
55from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
56from cura.Arranging.Nest2DArrange import arrange
57from cura.Machines.MachineErrorChecker import MachineErrorChecker
58from cura.Machines.Models.BuildPlateModel import BuildPlateModel
59from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
60from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
61from cura.Machines.Models.DiscoveredCloudPrintersModel import DiscoveredCloudPrintersModel
62from cura.Machines.Models.ExtrudersModel import ExtrudersModel
63from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
64from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
65from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
66from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
67from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
68from cura.Machines.Models.IntentModel import IntentModel
69from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
70from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
71from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
72from cura.Machines.Models.NozzleModel import NozzleModel
73from cura.Machines.Models.QualityManagementModel import QualityManagementModel
74from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
75from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
76from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
77from cura.Machines.Models.UserChangesModel import UserChangesModel
78from cura.Operations.SetParentOperation import SetParentOperation
79from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
80from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
81from cura.Scene import ZOffsetDecorator
82from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
83from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
84from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
85from cura.Scene.CuraSceneController import CuraSceneController
86from cura.Scene.CuraSceneNode import CuraSceneNode
87from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
88from cura.Settings.ContainerManager import ContainerManager
89from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
90from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
91from cura.Settings.ExtruderManager import ExtruderManager
92from cura.Settings.ExtruderStack import ExtruderStack
93from cura.Settings.GlobalStack import GlobalStack
94from cura.Settings.IntentManager import IntentManager
95from cura.Settings.MachineManager import MachineManager
96from cura.Settings.MachineNameValidator import MachineNameValidator
97from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
98from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
99from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
100from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
101from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
102from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
103from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
104from cura.UI.MachineSettingsManager import MachineSettingsManager
105from cura.UI.ObjectsModel import ObjectsModel
106from cura.UI.RecommendedMode import RecommendedMode
107from cura.UI.TextManager import TextManager
108from cura.UI.WelcomePagesModel import WelcomePagesModel
109from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
110from cura.UltimakerCloud import UltimakerCloudConstants
111from cura.Utils.NetworkingUtil import NetworkingUtil
112from . import BuildVolume
113from . import CameraAnimation
114from . import CuraActions
115from . import PlatformPhysics
116from . import PrintJobPreviewImageProvider
117from .AutoSave import AutoSave
118from .SingleInstance import SingleInstance
119
120if TYPE_CHECKING:
121    from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
122
123numpy.seterr(all = "ignore")
124
125
126class CuraApplication(QtApplication):
127    # SettingVersion represents the set of settings available in the machine/extruder definitions.
128    # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
129    # changes of the settings.
130    SettingVersion = 16
131
132    Created = False
133
134    class ResourceTypes:
135        QmlFiles = Resources.UserType + 1
136        Firmware = Resources.UserType + 2
137        QualityInstanceContainer = Resources.UserType + 3
138        QualityChangesInstanceContainer = Resources.UserType + 4
139        MaterialInstanceContainer = Resources.UserType + 5
140        VariantInstanceContainer = Resources.UserType + 6
141        UserInstanceContainer = Resources.UserType + 7
142        MachineStack = Resources.UserType + 8
143        ExtruderStack = Resources.UserType + 9
144        DefinitionChangesContainer = Resources.UserType + 10
145        SettingVisibilityPreset = Resources.UserType + 11
146        IntentInstanceContainer = Resources.UserType + 12
147
148    Q_ENUMS(ResourceTypes)
149
150    def __init__(self, *args, **kwargs):
151        super().__init__(name = ApplicationMetadata.CuraAppName,
152                         app_display_name = ApplicationMetadata.CuraAppDisplayName,
153                         version = ApplicationMetadata.CuraVersion,
154                         api_version = ApplicationMetadata.CuraSDKVersion,
155                         build_type = ApplicationMetadata.CuraBuildType,
156                         is_debug_mode = ApplicationMetadata.CuraDebugMode,
157                         tray_icon_name = "cura-icon-32.png",
158                         **kwargs)
159
160        self.default_theme = "cura-light"
161
162        self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
163
164        self._boot_loading_time = time.time()
165
166        self._on_exit_callback_manager = OnExitCallbackManager(self)
167
168        # Variables set from CLI
169        self._files_to_open = []
170        self._use_single_instance = False
171
172        self._single_instance = None
173
174        self._cura_formula_functions = None  # type: Optional[CuraFormulaFunctions]
175
176        self._machine_action_manager = None  # type: Optional[MachineActionManager.MachineActionManager]
177
178        self.empty_container = None  # type: EmptyInstanceContainer
179        self.empty_definition_changes_container = None  # type: EmptyInstanceContainer
180        self.empty_variant_container = None  # type: EmptyInstanceContainer
181        self.empty_intent_container = None  # type: EmptyInstanceContainer
182        self.empty_material_container = None  # type: EmptyInstanceContainer
183        self.empty_quality_container = None  # type: EmptyInstanceContainer
184        self.empty_quality_changes_container = None  # type: EmptyInstanceContainer
185
186        self._material_manager = None
187        self._machine_manager = None
188        self._extruder_manager = None
189        self._container_manager = None
190
191        self._object_manager = None
192        self._extruders_model = None
193        self._extruders_model_with_optional = None
194        self._build_plate_model = None
195        self._multi_build_plate_model = None
196        self._setting_visibility_presets_model = None
197        self._setting_inheritance_manager = None
198        self._simple_mode_settings_manager = None
199        self._cura_scene_controller = None
200        self._machine_error_checker = None
201
202        self._machine_settings_manager = MachineSettingsManager(self, parent = self)
203        self._material_management_model = None
204        self._quality_management_model = None
205
206        self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
207        self._discovered_cloud_printers_model = DiscoveredCloudPrintersModel(self, parent = self)
208        self._first_start_machine_actions_model = None
209        self._welcome_pages_model = WelcomePagesModel(self, parent = self)
210        self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
211        self._add_printer_pages_model_without_cancel = AddPrinterPagesModel(self, parent = self)
212        self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
213        self._text_manager = TextManager(parent = self)
214
215        self._quality_profile_drop_down_menu_model = None
216        self._custom_quality_profile_drop_down_menu_model = None
217        self._cura_API = CuraAPI(self)
218
219        self._physics = None
220        self._volume = None
221        self._output_devices = {}
222        self._print_information = None
223        self._previous_active_tool = None
224        self._platform_activity = False
225        self._scene_bounding_box = AxisAlignedBox.Null
226
227        self._center_after_select = False
228        self._camera_animation = None
229        self._cura_actions = None
230        self.started = False
231
232        self._message_box_callback = None
233        self._message_box_callback_arguments = []
234        self._i18n_catalog = None
235
236        self._currently_loading_files = []
237        self._non_sliceable_extensions = []
238        self._additional_components = {}  # Components to add to certain areas in the interface
239
240        self._open_file_queue = []  # A list of files to open (after the application has started)
241
242        self._update_platform_activity_timer = None
243
244        self._sidebar_custom_menu_items = []  # type: list # Keeps list of custom menu items for the side bar
245
246        self._plugins_loaded = False
247
248        # Backups
249        self._auto_save = None  # type: Optional[AutoSave]
250        self._enable_save = True
251
252        self._container_registry_class = CuraContainerRegistry
253        # Redefined here in order to please the typing.
254        self._container_registry = None # type: CuraContainerRegistry
255        from cura.CuraPackageManager import CuraPackageManager
256        self._package_manager_class = CuraPackageManager
257
258    @pyqtProperty(str, constant=True)
259    def ultimakerCloudApiRootUrl(self) -> str:
260        return UltimakerCloudConstants.CuraCloudAPIRoot
261
262    @pyqtProperty(str, constant = True)
263    def ultimakerCloudAccountRootUrl(self) -> str:
264        return UltimakerCloudConstants.CuraCloudAccountAPIRoot
265
266    @pyqtProperty(str, constant=True)
267    def ultimakerDigitalFactoryUrl(self) -> str:
268        return UltimakerCloudConstants.CuraDigitalFactoryURL
269
270    def addCommandLineOptions(self):
271        """Adds command line options to the command line parser.
272
273        This should be called after the application is created and before the pre-start.
274        """
275
276        super().addCommandLineOptions()
277        self._cli_parser.add_argument("--help", "-h",
278                                      action = "store_true",
279                                      default = False,
280                                      help = "Show this help message and exit.")
281        self._cli_parser.add_argument("--single-instance",
282                                      dest = "single_instance",
283                                      action = "store_true",
284                                      default = False)
285        # >> For debugging
286        # Trigger an early crash, i.e. a crash that happens before the application enters its event loop.
287        self._cli_parser.add_argument("--trigger-early-crash",
288                                      dest = "trigger_early_crash",
289                                      action = "store_true",
290                                      default = False,
291                                      help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
292        self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
293
294    def getContainerRegistry(self) -> "CuraContainerRegistry":
295        return self._container_registry
296
297    def parseCliOptions(self):
298        super().parseCliOptions()
299
300        if self._cli_args.help:
301            self._cli_parser.print_help()
302            sys.exit(0)
303
304        self._use_single_instance = self._cli_args.single_instance
305        # FOR TESTING ONLY
306        if self._cli_args.trigger_early_crash:
307            assert not "This crash is triggered by the trigger_early_crash command line argument."
308
309        for filename in self._cli_args.file:
310            self._files_to_open.append(os.path.abspath(filename))
311
312    def initialize(self) -> None:
313        self.__addExpectedResourceDirsAndSearchPaths()  # Must be added before init of super
314
315        super().initialize()
316
317        self._preferences.addPreference("cura/single_instance", False)
318        self._use_single_instance = self._preferences.getValue("cura/single_instance")
319
320        self.__sendCommandToSingleInstance()
321        self._initializeSettingDefinitions()
322        self._initializeSettingFunctions()
323        self.__addAllResourcesAndContainerResources()
324        self.__addAllEmptyContainers()
325        self.__setLatestResouceVersionsForVersionUpgrade()
326
327        self._machine_action_manager = MachineActionManager.MachineActionManager(self)
328        self._machine_action_manager.initialize()
329
330    def __sendCommandToSingleInstance(self):
331        self._single_instance = SingleInstance(self, self._files_to_open)
332
333        # If we use single instance, try to connect to the single instance server, send commands, and then exit.
334        # If we cannot find an existing single instance server, this is the only instance, so just keep going.
335        if self._use_single_instance:
336            if self._single_instance.startClient():
337                Logger.log("i", "Single instance commands were sent, exiting")
338                sys.exit(0)
339
340    def __addExpectedResourceDirsAndSearchPaths(self):
341        """Adds expected directory names and search paths for Resources."""
342
343        # this list of dir names will be used by UM to detect an old cura directory
344        for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
345            Resources.addExpectedDirNameInData(dir_name)
346
347        app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
348        Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
349
350        Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
351        if not hasattr(sys, "frozen"):
352            resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
353            Resources.addSearchPath(resource_path)
354
355    @classmethod
356    def _initializeSettingDefinitions(cls):
357        # Need to do this before ContainerRegistry tries to load the machines
358        SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default=True,
359                                               read_only=True)
360        SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default=True,
361                                               read_only=True)
362        # this setting can be changed for each group in one-at-a-time mode
363        SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default=True,
364                                               read_only=True)
365        SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default=True,
366                                               read_only=True)
367
368        # From which stack the setting would inherit if not defined per object (handled in the engine)
369        # AND for settings which are not settable_per_mesh:
370        # which extruder is the only extruder this setting is obtained from
371        SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default="-1",
372                                               depends_on="value")
373
374        # For settings which are not settable_per_mesh and not settable_per_extruder:
375        # A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
376        SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default=None,
377                                               depends_on="value")
378
379        SettingDefinition.addSettingType("extruder", None, str, Validator)
380        SettingDefinition.addSettingType("optional_extruder", None, str, None)
381        SettingDefinition.addSettingType("[int]", None, str, None)
382
383
384    def _initializeSettingFunctions(self):
385        """Adds custom property types, settings types, and extra operators (functions).
386
387        Whom need to be registered in SettingDefinition and SettingFunction.
388        """
389
390        self._cura_formula_functions = CuraFormulaFunctions(self)
391
392        SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
393        SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
394        SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
395        SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
396        SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
397        SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
398
399    def __addAllResourcesAndContainerResources(self) -> None:
400        """Adds all resources and container related resources."""
401
402        Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
403        Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
404        Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
405        Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
406        Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
407        Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
408        Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
409        Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
410        Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
411        Resources.addStorageType(self.ResourceTypes.IntentInstanceContainer, "intent")
412
413        self._container_registry.addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
414        self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
415        self._container_registry.addResourceType(self.ResourceTypes.VariantInstanceContainer, "variant")
416        self._container_registry.addResourceType(self.ResourceTypes.MaterialInstanceContainer, "material")
417        self._container_registry.addResourceType(self.ResourceTypes.UserInstanceContainer, "user")
418        self._container_registry.addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
419        self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine")
420        self._container_registry.addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
421        self._container_registry.addResourceType(self.ResourceTypes.IntentInstanceContainer, "intent")
422
423        Resources.addType(self.ResourceTypes.QmlFiles, "qml")
424        Resources.addType(self.ResourceTypes.Firmware, "firmware")
425
426    def __addAllEmptyContainers(self) -> None:
427        """Adds all empty containers."""
428
429        # Add empty variant, material and quality containers.
430        # Since they are empty, they should never be serialized and instead just programmatically created.
431        # We need them to simplify the switching between materials.
432        self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container
433
434        self._container_registry.addContainer(
435            cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
436        self.empty_definition_changes_container = cura.Settings.cura_empty_instance_containers.empty_definition_changes_container
437
438        self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container)
439        self.empty_variant_container = cura.Settings.cura_empty_instance_containers.empty_variant_container
440
441        self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_intent_container)
442        self.empty_intent_container = cura.Settings.cura_empty_instance_containers.empty_intent_container
443
444        self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_material_container)
445        self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
446
447        self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_container)
448        self.empty_quality_container = cura.Settings.cura_empty_instance_containers.empty_quality_container
449
450        self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container)
451        self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container
452
453    def __setLatestResouceVersionsForVersionUpgrade(self):
454        """Initializes the version upgrade manager with by providing the paths for each resource type and the latest
455        versions. """
456
457        self._version_upgrade_manager.setCurrentVersions(
458            {
459                ("quality", InstanceContainer.Version * 1000000 + self.SettingVersion):             (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
460                ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):     (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"),
461                ("intent", InstanceContainer.Version * 1000000 + self.SettingVersion):              (self.ResourceTypes.IntentInstanceContainer, "application/x-uranium-instancecontainer"),
462                ("machine_stack", GlobalStack.Version * 1000000 + self.SettingVersion):             (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
463                ("extruder_train", ExtruderStack.Version * 1000000 + self.SettingVersion):          (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
464                ("preferences", Preferences.Version * 1000000 + self.SettingVersion):               (Resources.Preferences, "application/x-uranium-preferences"),
465                ("user", InstanceContainer.Version * 1000000 + self.SettingVersion):                (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
466                ("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):  (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
467                ("variant", InstanceContainer.Version * 1000000 + self.SettingVersion):             (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"),
468            }
469        )
470
471    def startSplashWindowPhase(self) -> None:
472        """Runs preparations that needs to be done before the starting process."""
473
474        super().startSplashWindowPhase()
475
476        if not self.getIsHeadLess():
477            try:
478                self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
479            except FileNotFoundError:
480                Logger.log("w", "Unable to find the window icon.")
481
482        self.setRequiredPlugins([
483            # Misc.:
484            "ConsoleLogger", #You want to be able to read the log if something goes wrong.
485            "CuraEngineBackend", #Cura is useless without this one since you can't slice.
486            "FileLogger", #You want to be able to read the log if something goes wrong.
487            "XmlMaterialProfile", #Cura crashes without this one.
488            "Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
489            "PrepareStage", #Cura is useless without this one since you can't load models.
490            "PreviewStage", #This shows the list of the plugin views that are installed in Cura.
491            "MonitorStage", #Major part of Cura's functionality.
492            "LocalFileOutputDevice", #Major part of Cura's functionality.
493            "LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
494
495            # Views:
496            "SimpleView", #Dependency of SolidView.
497            "SolidView", #Displays models. Cura is useless without it.
498
499            # Readers & Writers:
500            "GCodeWriter", #Cura is useless if it can't write its output.
501            "STLReader", #Most common model format, so disabling this makes Cura 90% useless.
502            "3MFWriter", #Required for writing project files.
503
504            # Tools:
505            "CameraTool", #Needed to see the scene. Cura is useless without it.
506            "SelectionTool", #Dependency of the rest of the tools.
507            "TranslateTool", #You'll need this for almost every print.
508        ])
509        self._i18n_catalog = i18nCatalog("cura")
510
511        self._update_platform_activity_timer = QTimer()
512        self._update_platform_activity_timer.setInterval(500)
513        self._update_platform_activity_timer.setSingleShot(True)
514        self._update_platform_activity_timer.timeout.connect(self.updatePlatformActivity)
515
516        self.getController().getScene().sceneChanged.connect(self.updatePlatformActivityDelayed)
517        self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
518        self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
519        self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed)
520
521        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
522
523        self._container_registry.allMetadataLoaded.connect(ContainerRegistry.getInstance)
524
525        with self._container_registry.lockFile():
526            self._container_registry.loadAllMetadata()
527
528        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up preferences..."))
529        # Set the setting version for Preferences
530        preferences = self.getPreferences()
531        preferences.addPreference("metadata/setting_version", 0)
532        preferences.setValue("metadata/setting_version", self.SettingVersion)  # Don't make it equal to the default so that the setting version always gets written to the file.
533
534        preferences.addPreference("cura/active_mode", "simple")
535
536        preferences.addPreference("cura/categories_expanded", "")
537        preferences.addPreference("cura/jobname_prefix", True)
538        preferences.addPreference("cura/select_models_on_load", False)
539        preferences.addPreference("view/center_on_select", False)
540        preferences.addPreference("mesh/scale_to_fit", False)
541        preferences.addPreference("mesh/scale_tiny_meshes", True)
542        preferences.addPreference("cura/dialog_on_project_save", True)
543        preferences.addPreference("cura/asked_dialog_on_project_save", False)
544        preferences.addPreference("cura/choice_on_profile_override", "always_ask")
545        preferences.addPreference("cura/choice_on_open_project", "always_ask")
546        preferences.addPreference("cura/use_multi_build_plate", False)
547        preferences.addPreference("cura/show_list_of_objects", False)
548        preferences.addPreference("view/settings_list_height", 400)
549        preferences.addPreference("view/settings_visible", False)
550        preferences.addPreference("view/settings_xpos", 0)
551        preferences.addPreference("view/settings_ypos", 56)
552        preferences.addPreference("view/colorscheme_xpos", 0)
553        preferences.addPreference("view/colorscheme_ypos", 56)
554        preferences.addPreference("cura/currency", "€")
555        preferences.addPreference("cura/material_settings", "{}")
556
557        preferences.addPreference("view/invert_zoom", False)
558        preferences.addPreference("view/filter_current_build_plate", False)
559        preferences.addPreference("cura/sidebar_collapsed", False)
560
561        preferences.addPreference("cura/favorite_materials", "")
562        preferences.addPreference("cura/expanded_brands", "")
563        preferences.addPreference("cura/expanded_types", "")
564
565        preferences.addPreference("general/accepted_user_agreement", False)
566
567        for key in [
568            "dialog_load_path",  # dialog_save_path is in LocalFileOutputDevicePlugin
569            "dialog_profile_path",
570            "dialog_material_path"]:
571
572            preferences.addPreference("local_file/%s" % key, os.path.expanduser("~/"))
573
574        preferences.setDefault("local_file/last_used_type", "text/x-gcode")
575
576        self.applicationShuttingDown.connect(self.saveSettings)
577        self.engineCreatedSignal.connect(self._onEngineCreated)
578
579        self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
580
581        CuraApplication.Created = True
582
583    def _onEngineCreated(self):
584        self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
585
586    @pyqtProperty(bool)
587    def needToShowUserAgreement(self) -> bool:
588        return not UM.Util.parseBool(self.getPreferences().getValue("general/accepted_user_agreement"))
589
590    @pyqtSlot(bool)
591    def setNeedToShowUserAgreement(self, set_value: bool = True) -> None:
592        self.getPreferences().setValue("general/accepted_user_agreement", str(not set_value))
593
594    @pyqtSlot(str, str)
595    def writeToLog(self, severity: str, message: str) -> None:
596        Logger.log(severity, message)
597
598    # DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
599    # pre-exit checks such as checking for in-progress USB printing, etc.
600    # Except for the 'Decline and close' in the 'User Agreement'-step in the Welcome-pages, that should be a hard exit.
601    @pyqtSlot()
602    def closeApplication(self) -> None:
603        Logger.log("i", "Close application")
604        main_window = self.getMainWindow()
605        if main_window is not None:
606            main_window.close()
607        else:
608            self.exit(0)
609
610    # This function first performs all upon-exit checks such as USB printing that is in progress.
611    # Use this to close the application.
612    @pyqtSlot()
613    def checkAndExitApplication(self) -> None:
614        self._on_exit_callback_manager.resetCurrentState()
615        self._on_exit_callback_manager.triggerNextCallback()
616
617    @pyqtSlot(result = bool)
618    def getIsAllChecksPassed(self) -> bool:
619        return self._on_exit_callback_manager.getIsAllChecksPassed()
620
621    def getOnExitCallbackManager(self) -> "OnExitCallbackManager":
622        return self._on_exit_callback_manager
623
624    def triggerNextExitCheck(self) -> None:
625        self._on_exit_callback_manager.triggerNextCallback()
626
627    showConfirmExitDialog = pyqtSignal(str, arguments = ["message"])
628
629    def setConfirmExitDialogCallback(self, callback: Callable) -> None:
630        self._confirm_exit_dialog_callback = callback
631
632    @pyqtSlot(bool)
633    def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
634        self._confirm_exit_dialog_callback(yes_or_no)
635
636    showPreferencesWindow = pyqtSignal()
637    """Signal to connect preferences action in QML"""
638
639    @pyqtSlot()
640    def showPreferences(self) -> None:
641        """Show the preferences window"""
642
643        self.showPreferencesWindow.emit()
644
645    # This is called by drag-and-dropping curapackage files.
646    @pyqtSlot(QUrl)
647    def installPackageViaDragAndDrop(self, file_url: str) -> Optional[str]:
648        filename = QUrl(file_url).toLocalFile()
649        return self._package_manager.installPackage(filename)
650
651    @override(Application)
652    def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
653        return self._global_container_stack
654
655    @override(Application)
656    def setGlobalContainerStack(self, stack: Optional["GlobalStack"]) -> None:
657        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing Active Machine..."))
658        super().setGlobalContainerStack(stack)
659
660    showMessageBox = pyqtSignal(str,str, str, str, int, int,
661                                arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"])
662    """A reusable dialogbox"""
663
664    def messageBox(self, title, text,
665                   informativeText = "",
666                   detailedText = "",
667                   buttons = QMessageBox.Ok,
668                   icon = QMessageBox.NoIcon,
669                   callback = None,
670                   callback_arguments = []
671                   ):
672        self._message_box_callback = callback
673        self._message_box_callback_arguments = callback_arguments
674        self.showMessageBox.emit(title, text, informativeText, detailedText, buttons, icon)
675
676    showDiscardOrKeepProfileChanges = pyqtSignal()
677
678    def discardOrKeepProfileChanges(self) -> bool:
679        has_user_interaction = False
680        choice = self.getPreferences().getValue("cura/choice_on_profile_override")
681        if choice == "always_discard":
682            # don't show dialog and DISCARD the profile
683            self.discardOrKeepProfileChangesClosed("discard")
684        elif choice == "always_keep":
685            # don't show dialog and KEEP the profile
686            self.discardOrKeepProfileChangesClosed("keep")
687        elif not self._is_headless:
688            # ALWAYS ask whether to keep or discard the profile
689            self.showDiscardOrKeepProfileChanges.emit()
690            has_user_interaction = True
691        return has_user_interaction
692
693    @pyqtSlot(str)
694    def discardOrKeepProfileChangesClosed(self, option: str) -> None:
695        global_stack = self.getGlobalContainerStack()
696        if option == "discard":
697            for extruder in global_stack.extruderList:
698                extruder.userChanges.clear()
699            global_stack.userChanges.clear()
700
701        # if the user decided to keep settings then the user settings should be re-calculated and validated for errors
702        # before slicing. To ensure that slicer uses right settings values
703        elif option == "keep":
704            for extruder in global_stack.extruderList:
705                extruder.userChanges.update()
706            global_stack.userChanges.update()
707
708    @pyqtSlot(int)
709    def messageBoxClosed(self, button):
710        if self._message_box_callback:
711            self._message_box_callback(button, *self._message_box_callback_arguments)
712            self._message_box_callback = None
713            self._message_box_callback_arguments = []
714
715    def enableSave(self, enable: bool):
716        self._enable_save = enable
717
718    # Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
719    def saveSettings(self) -> None:
720        if not self.started or not self._enable_save:
721            # Do not do saving during application start or when data should not be saved on quit.
722            return
723        ContainerRegistry.getInstance().saveDirtyContainers()
724        self.savePreferences()
725
726    def saveStack(self, stack):
727        if not self._enable_save:
728            return
729        ContainerRegistry.getInstance().saveContainer(stack)
730
731    @pyqtSlot(str, result = QUrl)
732    def getDefaultPath(self, key):
733        default_path = self.getPreferences().getValue("local_file/%s" % key)
734        return QUrl.fromLocalFile(default_path)
735
736    @pyqtSlot(str, str)
737    def setDefaultPath(self, key, default_path):
738        self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile())
739
740    def _loadPlugins(self) -> None:
741        """Handle loading of all plugin types (and the backend explicitly)
742
743        :py:class:`Uranium.UM.PluginRegistry`
744        """
745
746        self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
747
748        self._plugin_registry.addType("profile_reader", self._addProfileReader)
749        self._plugin_registry.addType("profile_writer", self._addProfileWriter)
750
751        if Platform.isLinux():
752            lib_suffixes = {"", "64", "32", "x32"}  # A few common ones on different distributions.
753        else:
754            lib_suffixes = {""}
755        for suffix in lib_suffixes:
756            self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
757        if not hasattr(sys, "frozen"):
758            self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
759            self._plugin_registry.loadPlugin("ConsoleLogger")
760
761        self._plugin_registry.loadPlugins()
762
763        if self.getBackend() is None:
764            raise RuntimeError("Could not load the backend plugin!")
765
766        self._plugins_loaded = True
767
768    def _setLoadingHint(self, hint: str):
769        """Set a short, user-friendly hint about current loading status.
770
771        The way this message is displayed depends on application state
772        """
773
774        if self.started:
775            Logger.info(hint)
776        else:
777            self.showSplashMessage(hint)
778
779    def run(self):
780        super().run()
781
782        Logger.log("i", "Initializing machine error checker")
783        self._machine_error_checker = MachineErrorChecker(self)
784        self._machine_error_checker.initialize()
785        self.processEvents()
786
787        Logger.log("i", "Initializing machine manager")
788        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing machine manager..."))
789        self.getMachineManager()
790        self.processEvents()
791
792        Logger.log("i", "Initializing container manager")
793        self._container_manager = ContainerManager(self)
794        self.processEvents()
795
796        # Check if we should run as single instance or not. If so, set up a local socket server which listener which
797        # coordinates multiple Cura instances and accepts commands.
798        if self._use_single_instance:
799            self.__setUpSingleInstanceServer()
800
801        # Setup scene and build volume
802        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume..."))
803        root = self.getController().getScene().getRoot()
804        self._volume = BuildVolume.BuildVolume(self, root)
805
806        # Ensure that the old style arranger still works.
807        Arrange.build_volume = self._volume
808
809        # initialize info objects
810        self._print_information = PrintInformation.PrintInformation(self)
811        self._cura_actions = CuraActions.CuraActions(self)
812        self.processEvents()
813        # Initialize setting visibility presets model.
814        self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
815
816        # Initialize Cura API
817        self._cura_API.initialize()
818        self.processEvents()
819        self._output_device_manager.start()
820        self._welcome_pages_model.initialize()
821        self._add_printer_pages_model.initialize()
822        self._add_printer_pages_model_without_cancel.initialize(cancellable = False)
823        self._whats_new_pages_model.initialize()
824
825        # Detect in which mode to run and execute that mode
826        if self._is_headless:
827            self.runWithoutGUI()
828        else:
829            self.runWithGUI()
830
831        self.started = True
832        self.initializationFinished.emit()
833        Logger.log("d", "Booting Cura took %s seconds", time.time() - self._boot_loading_time)
834
835        # For now use a timer to postpone some things that need to be done after the application and GUI are
836        # initialized, for example opening files because they may show dialogs which can be closed due to incomplete
837        # GUI initialization.
838        self._post_start_timer = QTimer(self)
839        self._post_start_timer.setInterval(1000)
840        self._post_start_timer.setSingleShot(True)
841        self._post_start_timer.timeout.connect(self._onPostStart)
842        self._post_start_timer.start()
843
844        self._auto_save = AutoSave(self)
845        self._auto_save.initialize()
846
847        self.exec_()
848
849    def __setUpSingleInstanceServer(self):
850        if self._use_single_instance:
851            self._single_instance.startServer()
852
853    def _onPostStart(self):
854        for file_name in self._files_to_open:
855            self.callLater(self._openFile, file_name)
856        for file_name in self._open_file_queue:  # Open all the files that were queued up while plug-ins were loading.
857            self.callLater(self._openFile, file_name)
858
859    initializationFinished = pyqtSignal()
860    showAddPrintersUncancellableDialog = pyqtSignal()  # Used to show the add printers dialog with a greyed background
861
862    def runWithoutGUI(self):
863        """Run Cura without GUI elements and interaction (server mode)."""
864
865        self.closeSplash()
866
867    def runWithGUI(self):
868        """Run Cura with GUI (desktop mode)."""
869
870        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
871
872        controller = self.getController()
873
874        t = controller.getTool("TranslateTool")
875        if t:
876            t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
877
878        Selection.selectionChanged.connect(self.onSelectionChanged)
879
880        # Set default background color for scene
881        self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
882        self.processEvents()
883        # Initialize platform physics
884        self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
885
886        # Initialize camera
887        root = controller.getScene().getRoot()
888        camera = Camera("3d", root)
889        diagonal = self.getBuildVolume().getDiagonalSize()
890        if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
891            diagonal = 375
892        camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
893        camera.lookAt(Vector(0, 0, 0))
894        controller.getScene().setActiveCamera("3d")
895
896        # Initialize camera tool
897        camera_tool = controller.getTool("CameraTool")
898        if camera_tool:
899            camera_tool.setOrigin(Vector(0, 100, 0))
900            camera_tool.setZoomRange(0.1, 2000)
901
902        # Initialize camera animations
903        self._camera_animation = CameraAnimation.CameraAnimation()
904        self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
905
906        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
907
908        # Initialize QML engine
909        self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
910        self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
911        self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine..."))
912        self.initializeEngine()
913
914        # Initialize UI state
915        controller.setActiveStage("PrepareStage")
916        controller.setActiveView("SolidView")
917        controller.setCameraTool("CameraTool")
918        controller.setSelectionTool("SelectionTool")
919
920        # Hide the splash screen
921        self.closeSplash()
922
923    @pyqtSlot(result = QObject)
924    def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
925        return self._discovered_printer_model
926
927    @pyqtSlot(result=QObject)
928    def getDiscoveredCloudPrintersModel(self, *args) -> "DiscoveredCloudPrintersModel":
929        return self._discovered_cloud_printers_model
930
931    @pyqtSlot(result = QObject)
932    def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
933        if self._first_start_machine_actions_model is None:
934            self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
935            if self.started:
936                self._first_start_machine_actions_model.initialize()
937        return self._first_start_machine_actions_model
938
939    @pyqtSlot(result = QObject)
940    def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
941        return self._setting_visibility_presets_model
942
943    @pyqtSlot(result = QObject)
944    def getWelcomePagesModel(self, *args) -> "WelcomePagesModel":
945        return self._welcome_pages_model
946
947    @pyqtSlot(result = QObject)
948    def getAddPrinterPagesModel(self, *args) -> "AddPrinterPagesModel":
949        return self._add_printer_pages_model
950
951    @pyqtSlot(result = QObject)
952    def getAddPrinterPagesModelWithoutCancel(self, *args) -> "AddPrinterPagesModel":
953        return self._add_printer_pages_model_without_cancel
954
955    @pyqtSlot(result = QObject)
956    def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel":
957        return self._whats_new_pages_model
958
959    @pyqtSlot(result = QObject)
960    def getMachineSettingsManager(self, *args) -> "MachineSettingsManager":
961        return self._machine_settings_manager
962
963    @pyqtSlot(result = QObject)
964    def getTextManager(self, *args) -> "TextManager":
965        return self._text_manager
966
967    def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
968        if self._cura_formula_functions is None:
969            self._cura_formula_functions = CuraFormulaFunctions(self)
970        return self._cura_formula_functions
971
972    def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
973        return self._machine_error_checker
974
975    def getMachineManager(self, *args) -> MachineManager:
976        if self._machine_manager is None:
977            self._machine_manager = MachineManager(self, parent = self)
978        return self._machine_manager
979
980    def getExtruderManager(self, *args) -> ExtruderManager:
981        if self._extruder_manager is None:
982            self._extruder_manager = ExtruderManager()
983        return self._extruder_manager
984
985    def getIntentManager(self, *args) -> IntentManager:
986        return IntentManager.getInstance()
987
988    def getObjectsModel(self, *args):
989        if self._object_manager is None:
990            self._object_manager = ObjectsModel(self)
991        return self._object_manager
992
993    @pyqtSlot(result = QObject)
994    def getExtrudersModel(self, *args) -> "ExtrudersModel":
995        if self._extruders_model is None:
996            self._extruders_model = ExtrudersModel(self)
997        return self._extruders_model
998
999    @pyqtSlot(result = QObject)
1000    def getExtrudersModelWithOptional(self, *args) -> "ExtrudersModel":
1001        if self._extruders_model_with_optional is None:
1002            self._extruders_model_with_optional = ExtrudersModel(self)
1003            self._extruders_model_with_optional.setAddOptionalExtruder(True)
1004        return self._extruders_model_with_optional
1005
1006    @pyqtSlot(result = QObject)
1007    def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
1008        if self._multi_build_plate_model is None:
1009            self._multi_build_plate_model = MultiBuildPlateModel(self)
1010        return self._multi_build_plate_model
1011
1012    @pyqtSlot(result = QObject)
1013    def getBuildPlateModel(self, *args) -> BuildPlateModel:
1014        if self._build_plate_model is None:
1015            self._build_plate_model = BuildPlateModel(self)
1016        return self._build_plate_model
1017
1018    def getCuraSceneController(self, *args) -> CuraSceneController:
1019        if self._cura_scene_controller is None:
1020            self._cura_scene_controller = CuraSceneController.createCuraSceneController()
1021        return self._cura_scene_controller
1022
1023    def getSettingInheritanceManager(self, *args) -> SettingInheritanceManager:
1024        if self._setting_inheritance_manager is None:
1025            self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
1026        return self._setting_inheritance_manager
1027
1028    def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager:
1029        """Get the machine action manager
1030
1031        We ignore any *args given to this, as we also register the machine manager as qml singleton.
1032        It wants to give this function an engine and script engine, but we don't care about that.
1033        """
1034
1035        return cast(MachineActionManager.MachineActionManager, self._machine_action_manager)
1036
1037    @pyqtSlot(result = QObject)
1038    def getMaterialManagementModel(self) -> MaterialManagementModel:
1039        if not self._material_management_model:
1040            self._material_management_model = MaterialManagementModel(parent = self)
1041        return self._material_management_model
1042
1043    @pyqtSlot(result = QObject)
1044    def getQualityManagementModel(self) -> QualityManagementModel:
1045        if not self._quality_management_model:
1046            self._quality_management_model = QualityManagementModel(parent = self)
1047        return self._quality_management_model
1048
1049    def getSimpleModeSettingsManager(self, *args):
1050        if self._simple_mode_settings_manager is None:
1051            self._simple_mode_settings_manager = SimpleModeSettingsManager()
1052        return self._simple_mode_settings_manager
1053
1054    def event(self, event):
1055        """Handle Qt events"""
1056
1057        if event.type() == QEvent.FileOpen:
1058            if self._plugins_loaded:
1059                self._openFile(event.file())
1060            else:
1061                self._open_file_queue.append(event.file())
1062
1063        return super().event(event)
1064
1065    def getAutoSave(self) -> Optional[AutoSave]:
1066        return self._auto_save
1067
1068    def getPrintInformation(self):
1069        """Get print information (duration / material used)"""
1070
1071        return self._print_information
1072
1073    def getQualityProfilesDropDownMenuModel(self, *args, **kwargs):
1074        if self._quality_profile_drop_down_menu_model is None:
1075            self._quality_profile_drop_down_menu_model = QualityProfilesDropDownMenuModel(self)
1076        return self._quality_profile_drop_down_menu_model
1077
1078    def getCustomQualityProfilesDropDownMenuModel(self, *args, **kwargs):
1079        if self._custom_quality_profile_drop_down_menu_model is None:
1080            self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
1081        return self._custom_quality_profile_drop_down_menu_model
1082
1083    def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
1084        return self._cura_API
1085
1086    def registerObjects(self, engine):
1087        """Registers objects for the QML engine to use.
1088
1089        :param engine: The QML engine.
1090        """
1091
1092        super().registerObjects(engine)
1093
1094        # global contexts
1095        self.processEvents()
1096        engine.rootContext().setContextProperty("Printer", self)
1097        engine.rootContext().setContextProperty("CuraApplication", self)
1098        engine.rootContext().setContextProperty("PrintInformation", self._print_information)
1099        engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
1100        engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
1101
1102        self.processEvents()
1103        qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
1104
1105        self.processEvents()
1106        qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController)
1107        qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
1108        qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
1109        qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, "IntentManager", self.getIntentManager)
1110        qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
1111        qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
1112        qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
1113
1114        self.processEvents()
1115        qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
1116        qmlRegisterType(WelcomePagesModel, "Cura", 1, 0, "WelcomePagesModel")
1117        qmlRegisterType(WhatsNewPagesModel, "Cura", 1, 0, "WhatsNewPagesModel")
1118        qmlRegisterType(AddPrinterPagesModel, "Cura", 1, 0, "AddPrinterPagesModel")
1119        qmlRegisterType(TextManager, "Cura", 1, 0, "TextManager")
1120        qmlRegisterType(RecommendedMode, "Cura", 1, 0, "RecommendedMode")
1121
1122        self.processEvents()
1123        qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
1124        qmlRegisterType(ObjectsModel, "Cura", 1, 0, "ObjectsModel")
1125        qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
1126        qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
1127        qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
1128        qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
1129        qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
1130
1131        self.processEvents()
1132        qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
1133        qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
1134        qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
1135        qmlRegisterSingletonType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel", self.getQualityManagementModel)
1136        qmlRegisterSingletonType(MaterialManagementModel, "Cura", 1, 5, "MaterialManagementModel", self.getMaterialManagementModel)
1137
1138        self.processEvents()
1139        qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
1140        qmlRegisterType(DiscoveredCloudPrintersModel, "Cura", 1, 7, "DiscoveredCloudPrintersModel")
1141        qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
1142                                 "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
1143        qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
1144                                 "CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel)
1145        qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
1146        qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
1147        qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
1148
1149        self.processEvents()
1150        qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
1151        qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
1152        qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
1153        qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
1154        qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
1155        qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
1156        qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
1157        qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
1158
1159        qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
1160
1161        from cura.API import CuraAPI
1162        qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
1163        qmlRegisterUncreatableType(Account, "Cura", 1, 0, "AccountSyncState", "Could not create AccountSyncState")
1164
1165        # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
1166        actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
1167        qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
1168
1169        for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
1170            type_name = os.path.splitext(os.path.basename(path))[0]
1171            if type_name in ("Cura", "Actions"):
1172                continue
1173
1174            # Ignore anything that is not a QML file.
1175            if not path.endswith(".qml"):
1176                continue
1177
1178            qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
1179            self.processEvents()
1180
1181    def onSelectionChanged(self):
1182        if Selection.hasSelection():
1183            if self.getController().getActiveTool():
1184                # If the tool has been disabled by the new selection
1185                if not self.getController().getActiveTool().getEnabled():
1186                    # Default
1187                    self.getController().setActiveTool("TranslateTool")
1188            else:
1189                if self._previous_active_tool:
1190                    self.getController().setActiveTool(self._previous_active_tool)
1191                    if not self.getController().getActiveTool().getEnabled():
1192                        self.getController().setActiveTool("TranslateTool")
1193                    self._previous_active_tool = None
1194                else:
1195                    # Default
1196                    self.getController().setActiveTool("TranslateTool")
1197
1198            if self.getPreferences().getValue("view/center_on_select"):
1199                self._center_after_select = True
1200        else:
1201            if self.getController().getActiveTool():
1202                self._previous_active_tool = self.getController().getActiveTool().getPluginId()
1203                self.getController().setActiveTool(None)
1204
1205    def _onToolOperationStopped(self, event):
1206        if self._center_after_select and Selection.getSelectedObject(0) is not None:
1207            self._center_after_select = False
1208            self._camera_animation.setStart(self.getController().getTool("CameraTool").getOrigin())
1209            self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
1210            self._camera_animation.start()
1211
1212    activityChanged = pyqtSignal()
1213    sceneBoundingBoxChanged = pyqtSignal()
1214
1215    @pyqtProperty(bool, notify = activityChanged)
1216    def platformActivity(self):
1217        return self._platform_activity
1218
1219    @pyqtProperty(str, notify = sceneBoundingBoxChanged)
1220    def getSceneBoundingBoxString(self):
1221        return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
1222
1223    def updatePlatformActivityDelayed(self, node = None):
1224        if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")):
1225            self._update_platform_activity_timer.start()
1226
1227    def updatePlatformActivity(self, node = None):
1228        """Update scene bounding box for current build plate"""
1229
1230        count = 0
1231        scene_bounding_box = None
1232        is_block_slicing_node = False
1233        active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
1234
1235        print_information = self.getPrintInformation()
1236        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1237            if (
1238                not issubclass(type(node), CuraSceneNode) or
1239                (not node.getMeshData() and not node.callDecoration("getLayerData")) or
1240                (node.callDecoration("getBuildPlateNumber") != active_build_plate)):
1241
1242                continue
1243            if node.callDecoration("isBlockSlicing"):
1244                is_block_slicing_node = True
1245
1246            count += 1
1247
1248            # After clicking the Undo button, if the build plate empty the project name needs to be set
1249            if print_information.baseName == '':
1250                print_information.setBaseName(node.getName())
1251
1252            if not scene_bounding_box:
1253                scene_bounding_box = node.getBoundingBox()
1254            else:
1255                other_bb = node.getBoundingBox()
1256                if other_bb is not None:
1257                    scene_bounding_box = scene_bounding_box + node.getBoundingBox()
1258
1259
1260        if print_information:
1261            print_information.setPreSliced(is_block_slicing_node)
1262
1263        if not scene_bounding_box:
1264            scene_bounding_box = AxisAlignedBox.Null
1265
1266        if repr(self._scene_bounding_box) != repr(scene_bounding_box):
1267            self._scene_bounding_box = scene_bounding_box
1268            self.sceneBoundingBoxChanged.emit()
1269
1270        self._platform_activity = True if count > 0 else False
1271        self.activityChanged.emit()
1272
1273    @pyqtSlot()
1274    def selectAll(self):
1275        """Select all nodes containing mesh data in the scene."""
1276
1277        if not self.getController().getToolsEnabled():
1278            return
1279
1280        Selection.clear()
1281        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1282            if not isinstance(node, SceneNode):
1283                continue
1284            if not node.getMeshData() and not node.callDecoration("isGroup"):
1285                continue  # Node that doesnt have a mesh and is not a group.
1286            if node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent().callDecoration("isSliceable"):
1287                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
1288            if not node.isSelectable():
1289                continue  # i.e. node with layer data
1290            if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
1291                continue  # i.e. node with layer data
1292
1293            Selection.add(node)
1294
1295    @pyqtSlot()
1296    def resetAllTranslation(self):
1297        """Reset all translation on nodes with mesh data."""
1298
1299        Logger.log("i", "Resetting all scene translations")
1300        nodes = []
1301        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1302            if not isinstance(node, SceneNode):
1303                continue
1304            if not node.getMeshData() and not node.callDecoration("isGroup"):
1305                continue  # Node that doesnt have a mesh and is not a group.
1306            if node.getParent() and node.getParent().callDecoration("isGroup"):
1307                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
1308            if not node.isSelectable():
1309                continue  # i.e. node with layer data
1310            nodes.append(node)
1311
1312        if nodes:
1313            op = GroupedOperation()
1314            for node in nodes:
1315                # Ensure that the object is above the build platform
1316                node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
1317                if node.getBoundingBox():
1318                    center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
1319                else:
1320                    center_y = 0
1321                op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0)))
1322            op.push()
1323
1324    @pyqtSlot()
1325    def resetAll(self):
1326        """Reset all transformations on nodes with mesh data."""
1327
1328        Logger.log("i", "Resetting all scene transformations")
1329        nodes = []
1330        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1331            if not isinstance(node, SceneNode):
1332                continue
1333            if not node.getMeshData() and not node.callDecoration("isGroup"):
1334                continue  # Node that doesnt have a mesh and is not a group.
1335            if node.getParent() and node.getParent().callDecoration("isGroup"):
1336                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
1337            if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
1338                continue  # i.e. node with layer data
1339            nodes.append(node)
1340
1341        if nodes:
1342            op = GroupedOperation()
1343            for node in nodes:
1344                # Ensure that the object is above the build platform
1345                node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
1346                if node.getBoundingBox():
1347                    center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
1348                else:
1349                    center_y = 0
1350                op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
1351            op.push()
1352
1353    @pyqtSlot()
1354    def arrangeObjectsToAllBuildPlates(self) -> None:
1355        """Arrange all objects."""
1356
1357        nodes_to_arrange = []
1358        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1359            if not isinstance(node, SceneNode):
1360                continue
1361
1362            if not node.getMeshData() and not node.callDecoration("isGroup"):
1363                continue  # Node that doesnt have a mesh and is not a group.
1364
1365            parent_node = node.getParent()
1366            if parent_node and parent_node.callDecoration("isGroup"):
1367                continue  # Grouped nodes don't need resetting as their parent (the group) is reset)
1368
1369            if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
1370                continue  # i.e. node with layer data
1371
1372            bounding_box = node.getBoundingBox()
1373            # Skip nodes that are too big
1374            if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
1375                nodes_to_arrange.append(node)
1376        job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
1377        job.start()
1378        self.getCuraSceneController().setActiveBuildPlate(0)  # Select first build plate
1379
1380    # Single build plate
1381    @pyqtSlot()
1382    def arrangeAll(self) -> None:
1383        nodes_to_arrange = []
1384        active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
1385        locked_nodes = []
1386        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1387            if not isinstance(node, SceneNode):
1388                continue
1389
1390            if not node.getMeshData() and not node.callDecoration("isGroup"):
1391                continue  # Node that doesnt have a mesh and is not a group.
1392
1393            parent_node = node.getParent()
1394            if parent_node and parent_node.callDecoration("isGroup"):
1395                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
1396
1397            if not node.isSelectable():
1398                continue  # i.e. node with layer data
1399
1400            if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
1401                continue  # i.e. node with layer data
1402
1403            if node.callDecoration("getBuildPlateNumber") == active_build_plate:
1404                # Skip nodes that are too big
1405                bounding_box = node.getBoundingBox()
1406                if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
1407                    # Arrange only the unlocked nodes and keep the locked ones in place
1408                    if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)):
1409                        locked_nodes.append(node)
1410                    else:
1411                        nodes_to_arrange.append(node)
1412        self.arrange(nodes_to_arrange, locked_nodes)
1413
1414    def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
1415        """Arrange a set of nodes given a set of fixed nodes
1416
1417        :param nodes: nodes that we have to place
1418        :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes
1419        """
1420
1421        min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2  # Allow for some rounding errors
1422        job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
1423        job.start()
1424
1425    @pyqtSlot()
1426    def reloadAll(self) -> None:
1427        """Reload all mesh data on the screen from file."""
1428
1429        Logger.log("i", "Reloading all loaded mesh data.")
1430        nodes = []
1431        has_merged_nodes = False
1432        gcode_filename = None  # type: Optional[str]
1433        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1434            # Objects loaded from Gcode should also be included.
1435            gcode_filename = node.callDecoration("getGcodeFileName")
1436            if gcode_filename is not None:
1437                break
1438
1439            if not isinstance(node, CuraSceneNode) or not node.getMeshData():
1440                if node.getName() == "MergedMesh":
1441                    has_merged_nodes = True
1442                continue
1443
1444            nodes.append(node)
1445
1446        # We can open only one gcode file at the same time. If the current view has a gcode file open, just reopen it
1447        # for reloading.
1448        if gcode_filename:
1449            self._openFile(gcode_filename)
1450
1451        if not nodes:
1452            return
1453
1454        objects_in_filename = {}  # type: Dict[str, List[CuraSceneNode]]
1455        for node in nodes:
1456            mesh_data = node.getMeshData()
1457            if mesh_data:
1458                file_name = mesh_data.getFileName()
1459                if file_name:
1460                    if file_name not in objects_in_filename:
1461                        objects_in_filename[file_name] = []
1462                    if file_name in objects_in_filename:
1463                        objects_in_filename[file_name].append(node)
1464                else:
1465                    Logger.log("w", "Unable to reload data because we don't have a filename.")
1466
1467        for file_name, nodes in objects_in_filename.items():
1468            for node in nodes:
1469                job = ReadMeshJob(file_name)
1470                job._node = node  # type: ignore
1471                job.finished.connect(self._reloadMeshFinished)
1472                if has_merged_nodes:
1473                    job.finished.connect(self.updateOriginOfMergedMeshes)
1474
1475                job.start()
1476
1477    @pyqtSlot("QStringList")
1478    def setExpandedCategories(self, categories: List[str]) -> None:
1479        categories = list(set(categories))
1480        categories.sort()
1481        joined = ";".join(categories)
1482        if joined != self.getPreferences().getValue("cura/categories_expanded"):
1483            self.getPreferences().setValue("cura/categories_expanded", joined)
1484            self.expandedCategoriesChanged.emit()
1485
1486    expandedCategoriesChanged = pyqtSignal()
1487
1488    @pyqtProperty("QStringList", notify = expandedCategoriesChanged)
1489    def expandedCategories(self) -> List[str]:
1490        return self.getPreferences().getValue("cura/categories_expanded").split(";")
1491
1492    @pyqtSlot()
1493    def mergeSelected(self):
1494        self.groupSelected()
1495        try:
1496            group_node = Selection.getAllSelectedObjects()[0]
1497        except Exception as e:
1498            Logger.log("e", "mergeSelected: Exception: %s", e)
1499            return
1500
1501        meshes = [node.getMeshData() for node in group_node.getAllChildren() if node.getMeshData()]
1502
1503        # Compute the center of the objects
1504        object_centers = []
1505        # Forget about the translation that the original objects have
1506        zero_translation = Matrix(data=numpy.zeros(3))
1507        for mesh, node in zip(meshes, group_node.getChildren()):
1508            transformation = node.getLocalTransformation()
1509            transformation.setTranslation(zero_translation)
1510            transformed_mesh = mesh.getTransformed(transformation)
1511            center = transformed_mesh.getCenterPosition()
1512            if center is not None:
1513                object_centers.append(center)
1514
1515        if object_centers:
1516            middle_x = sum([v.x for v in object_centers]) / len(object_centers)
1517            middle_y = sum([v.y for v in object_centers]) / len(object_centers)
1518            middle_z = sum([v.z for v in object_centers]) / len(object_centers)
1519            offset = Vector(middle_x, middle_y, middle_z)
1520        else:
1521            offset = Vector(0, 0, 0)
1522
1523        # Move each node to the same position.
1524        for mesh, node in zip(meshes, group_node.getChildren()):
1525            node.setTransformation(Matrix())
1526            # Align the object around its zero position
1527            # and also apply the offset to center it inside the group.
1528            node.setPosition(-mesh.getZeroPosition() - offset)
1529
1530        # Use the previously found center of the group bounding box as the new location of the group
1531        group_node.setPosition(group_node.getBoundingBox().center)
1532        group_node.setName("MergedMesh")  # add a specific name to distinguish this node
1533
1534
1535    def updateOriginOfMergedMeshes(self, _):
1536        """Updates origin position of all merged meshes"""
1537
1538        group_nodes = []
1539        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1540            if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
1541
1542                # Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
1543                for decorator in node.getDecorators():
1544                    if isinstance(decorator, GroupDecorator):
1545                        group_nodes.append(node)
1546                        break
1547
1548        for group_node in group_nodes:
1549            meshes = [node.getMeshData() for node in group_node.getAllChildren() if node.getMeshData()]
1550
1551            # Compute the center of the objects
1552            object_centers = []
1553            # Forget about the translation that the original objects have
1554            zero_translation = Matrix(data=numpy.zeros(3))
1555            for mesh, node in zip(meshes, group_node.getChildren()):
1556                transformation = node.getLocalTransformation()
1557                transformation.setTranslation(zero_translation)
1558                transformed_mesh = mesh.getTransformed(transformation)
1559                center = transformed_mesh.getCenterPosition()
1560                if center is not None:
1561                    object_centers.append(center)
1562
1563            if object_centers:
1564                middle_x = sum([v.x for v in object_centers]) / len(object_centers)
1565                middle_y = sum([v.y for v in object_centers]) / len(object_centers)
1566                middle_z = sum([v.z for v in object_centers]) / len(object_centers)
1567                offset = Vector(middle_x, middle_y, middle_z)
1568            else:
1569                offset = Vector(0, 0, 0)
1570
1571            # Move each node to the same position.
1572            for mesh, node in zip(meshes, group_node.getChildren()):
1573                transformation = node.getLocalTransformation()
1574                transformation.setTranslation(zero_translation)
1575                transformed_mesh = mesh.getTransformed(transformation)
1576
1577                # Align the object around its zero position
1578                # and also apply the offset to center it inside the group.
1579                node.setPosition(-transformed_mesh.getZeroPosition() - offset)
1580
1581            # Use the previously found center of the group bounding box as the new location of the group
1582            group_node.setPosition(group_node.getBoundingBox().center)
1583
1584
1585    @pyqtSlot()
1586    def groupSelected(self) -> None:
1587        # Create a group-node
1588        group_node = CuraSceneNode()
1589        group_decorator = GroupDecorator()
1590        group_node.addDecorator(group_decorator)
1591        group_node.addDecorator(ConvexHullDecorator())
1592        group_node.addDecorator(BuildPlateDecorator(self.getMultiBuildPlateModel().activeBuildPlate))
1593        group_node.setParent(self.getController().getScene().getRoot())
1594        group_node.setSelectable(True)
1595        center = Selection.getSelectionCenter()
1596        group_node.setPosition(center)
1597        group_node.setCenterPosition(center)
1598
1599        # Remove nodes that are directly parented to another selected node from the selection so they remain parented
1600        selected_nodes = Selection.getAllSelectedObjects().copy()
1601        for node in selected_nodes:
1602            parent = node.getParent()
1603            if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
1604                Selection.remove(node)
1605
1606        # Move selected nodes into the group-node
1607        Selection.applyOperation(SetParentOperation, group_node)
1608
1609        # Deselect individual nodes and select the group-node instead
1610        for node in group_node.getChildren():
1611            Selection.remove(node)
1612        Selection.add(group_node)
1613
1614    @pyqtSlot()
1615    def ungroupSelected(self) -> None:
1616        selected_objects = Selection.getAllSelectedObjects().copy()
1617        for node in selected_objects:
1618            if node.callDecoration("isGroup"):
1619                op = GroupedOperation()
1620
1621                group_parent = node.getParent()
1622                children = node.getChildren().copy()
1623                for child in children:
1624                    # Ungroup only 1 level deep
1625                    if child.getParent() != node:
1626                        continue
1627
1628                    # Set the parent of the children to the parent of the group-node
1629                    op.addOperation(SetParentOperation(child, group_parent))
1630
1631                    # Add all individual nodes to the selection
1632                    Selection.add(child)
1633
1634                op.push()
1635                # Note: The group removes itself from the scene once all its children have left it,
1636                # see GroupDecorator._onChildrenChanged
1637
1638    def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
1639        if self._is_headless:
1640            return None
1641        return CuraSplashScreen.CuraSplashScreen()
1642
1643    def _onActiveMachineChanged(self):
1644        pass
1645
1646    fileLoaded = pyqtSignal(str)
1647    fileCompleted = pyqtSignal(str)
1648
1649    def _reloadMeshFinished(self, job) -> None:
1650        """
1651        Function called whenever a ReadMeshJob finishes in the background. It reloads a specific node object in the
1652        scene from its source file. The function gets all the nodes that exist in the file through the job result, and
1653        then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node.
1654
1655        :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the
1656        meshes in a file
1657        """
1658
1659        job_result = job.getResult()  # nodes that exist inside the file read by this job
1660        if len(job_result) == 0:
1661            Logger.log("e", "Reloading the mesh failed.")
1662            return
1663        object_found = False
1664        mesh_data = None
1665        # Find the node to be refreshed based on its id
1666        for job_result_node in job_result:
1667            if job_result_node.getId() == job._node.getId():
1668                mesh_data = job_result_node.getMeshData()
1669                object_found = True
1670                break
1671        if not object_found:
1672            Logger.warning("The object with id {} no longer exists! Keeping the old version in the scene.".format(job_result_node.getId()))
1673            return
1674        if not mesh_data:
1675            Logger.log("w", "Could not find a mesh in reloaded node.")
1676            return
1677        job._node.setMeshData(mesh_data)
1678
1679    def _openFile(self, filename):
1680        self.readLocalFile(QUrl.fromLocalFile(filename))
1681
1682    def _addProfileReader(self, profile_reader):
1683        # TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
1684        pass
1685
1686    def _addProfileWriter(self, profile_writer):
1687        pass
1688
1689    @pyqtSlot("QSize")
1690    def setMinimumWindowSize(self, size):
1691        main_window = self.getMainWindow()
1692        if main_window:
1693            main_window.setMinimumSize(size)
1694
1695    def getBuildVolume(self):
1696        return self._volume
1697
1698    additionalComponentsChanged = pyqtSignal(str, arguments = ["areaId"])
1699
1700    @pyqtProperty("QVariantMap", notify = additionalComponentsChanged)
1701    def additionalComponents(self):
1702        return self._additional_components
1703
1704    @pyqtSlot(str, "QVariant")
1705    def addAdditionalComponent(self, area_id: str, component):
1706        """Add a component to a list of components to be reparented to another area in the GUI.
1707
1708        The actual reparenting is done by the area itself.
1709        :param area_id: dentifying name of the area to which the component should be reparented
1710        :param (QQuickComponent) component: The component that should be reparented
1711        """
1712
1713        if area_id not in self._additional_components:
1714            self._additional_components[area_id] = []
1715        self._additional_components[area_id].append(component)
1716
1717        self.additionalComponentsChanged.emit(area_id)
1718
1719    @pyqtSlot(str)
1720    def log(self, msg):
1721        Logger.log("d", msg)
1722
1723    openProjectFile = pyqtSignal(QUrl, arguments = ["project_file"])  # Emitted when a project file is about to open.
1724
1725    @pyqtSlot(QUrl, str)
1726    @pyqtSlot(QUrl)
1727    def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None):
1728        """Open a local file
1729
1730        :param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model"
1731         or "open_as_project". This parameter is only considered if the file is a project file.
1732        """
1733        Logger.log("i", "Attempting to read file %s", file.toString())
1734        if not file.isValid():
1735            return
1736
1737        scene = self.getController().getScene()
1738
1739        for node in DepthFirstIterator(scene.getRoot()):
1740            if node.callDecoration("isBlockSlicing"):
1741                self.deleteAll()
1742                break
1743
1744        is_project_file = self.checkIsValidProjectFile(file)
1745
1746        if project_mode is None:
1747            project_mode = self.getPreferences().getValue("cura/choice_on_open_project")
1748
1749        if is_project_file and project_mode == "open_as_project":
1750            # open as project immediately without presenting a dialog
1751            workspace_handler = self.getWorkspaceFileHandler()
1752            workspace_handler.readLocalFile(file)
1753            return
1754
1755        if is_project_file and project_mode == "always_ask":
1756            # present a dialog asking to open as project or import models
1757            self.callLater(self.openProjectFile.emit, file)
1758            return
1759
1760        # Either the file is a model file or we want to load only models from project. Continue to load models.
1761
1762        if self.getPreferences().getValue("cura/select_models_on_load"):
1763            Selection.clear()
1764
1765        f = file.toLocalFile()
1766        extension = os.path.splitext(f)[1]
1767        extension = extension.lower()
1768        filename = os.path.basename(f)
1769        if self._currently_loading_files:
1770            # If a non-slicable file is already being loaded, we prevent loading of any further non-slicable files
1771            if extension in self._non_sliceable_extensions:
1772                message = Message(
1773                    self._i18n_catalog.i18nc("@info:status",
1774                                       "Only one G-code file can be loaded at a time. Skipped importing {0}",
1775                                       filename), title = self._i18n_catalog.i18nc("@info:title", "Warning"))
1776                message.show()
1777                return
1778            # If file being loaded is non-slicable file, then prevent loading of any other files
1779            extension = os.path.splitext(self._currently_loading_files[0])[1]
1780            extension = extension.lower()
1781            if extension in self._non_sliceable_extensions:
1782                message = Message(
1783                    self._i18n_catalog.i18nc("@info:status",
1784                                       "Can't open any other file if G-code is loading. Skipped importing {0}",
1785                                       filename), title = self._i18n_catalog.i18nc("@info:title", "Error"))
1786                message.show()
1787                return
1788
1789        self._currently_loading_files.append(f)
1790        if extension in self._non_sliceable_extensions:
1791            self.deleteAll(only_selectable = False)
1792
1793        job = ReadMeshJob(f)
1794        job.finished.connect(self._readMeshFinished)
1795        job.start()
1796
1797    def _readMeshFinished(self, job):
1798        global_container_stack = self.getGlobalContainerStack()
1799        if not global_container_stack:
1800            Logger.log("w", "Can't load meshes before a printer is added.")
1801            return
1802        if not self._volume:
1803            Logger.log("w", "Can't load meshes before the build volume is initialized")
1804            return
1805
1806        nodes = job.getResult()
1807        if nodes is None:
1808            Logger.error("Read mesh job returned None. Mesh loading must have failed.")
1809            return
1810        file_name = job.getFileName()
1811        file_name_lower = file_name.lower()
1812        file_extension = file_name_lower.split(".")[-1]
1813        self._currently_loading_files.remove(file_name)
1814
1815        self.fileLoaded.emit(file_name)
1816        target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
1817
1818        root = self.getController().getScene().getRoot()
1819        fixed_nodes = []
1820        for node_ in DepthFirstIterator(root):
1821            if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
1822                fixed_nodes.append(node_)
1823
1824        default_extruder_position = self.getMachineManager().defaultExtruderPosition
1825        default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId()
1826
1827        select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
1828
1829        nodes_to_arrange = []  # type: List[CuraSceneNode]
1830
1831        fixed_nodes = []
1832        for node_ in DepthFirstIterator(self.getController().getScene().getRoot()):
1833            # Only count sliceable objects
1834            if node_.callDecoration("isSliceable"):
1835                fixed_nodes.append(node_)
1836
1837        for original_node in nodes:
1838            # Create a CuraSceneNode just if the original node is not that type
1839            if isinstance(original_node, CuraSceneNode):
1840                node = original_node
1841            else:
1842                node = CuraSceneNode()
1843                node.setMeshData(original_node.getMeshData())
1844
1845                # Setting meshdata does not apply scaling.
1846                if original_node.getScale() != Vector(1.0, 1.0, 1.0):
1847                    node.scale(original_node.getScale())
1848
1849            node.setSelectable(True)
1850            node.setName(os.path.basename(file_name))
1851            self.getBuildVolume().checkBoundsAndUpdate(node)
1852
1853            is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
1854
1855            if is_non_sliceable:
1856                # Need to switch first to the preview stage and then to layer view
1857                self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
1858                                        self.getController().setActiveView("SimulationView")))
1859
1860                block_slicing_decorator = BlockSlicingDecorator()
1861                node.addDecorator(block_slicing_decorator)
1862            else:
1863                sliceable_decorator = SliceableObjectDecorator()
1864                node.addDecorator(sliceable_decorator)
1865
1866            scene = self.getController().getScene()
1867
1868            # If there is no convex hull for the node, start calculating it and continue.
1869            if not node.getDecorator(ConvexHullDecorator):
1870                node.addDecorator(ConvexHullDecorator())
1871            for child in node.getAllChildren():
1872                if not child.getDecorator(ConvexHullDecorator):
1873                    child.addDecorator(ConvexHullDecorator())
1874
1875            if file_extension != "3mf":
1876                if node.callDecoration("isSliceable"):
1877                    # Ensure that the bottom of the bounding box is on the build plate
1878                    if node.getBoundingBox():
1879                        center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
1880                    else:
1881                        center_y = 0
1882
1883                    node.translate(Vector(0, center_y, 0))
1884
1885                    nodes_to_arrange.append(node)
1886
1887            # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
1888            # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
1889            # the BuildPlateDecorator exists or not and always set the correct build plate number.
1890            build_plate_decorator = node.getDecorator(BuildPlateDecorator)
1891            if build_plate_decorator is None:
1892                build_plate_decorator = BuildPlateDecorator(target_build_plate)
1893                node.addDecorator(build_plate_decorator)
1894            build_plate_decorator.setBuildPlateNumber(target_build_plate)
1895
1896            operation = AddSceneNodeOperation(node, scene.getRoot())
1897            operation.push()
1898
1899            node.callDecoration("setActiveExtruder", default_extruder_id)
1900            scene.sceneChanged.emit(node)
1901
1902            if select_models_on_load:
1903                Selection.add(node)
1904        try:
1905            arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
1906        except:
1907            Logger.logException("e", "Failed to arrange the models")
1908        self.fileCompleted.emit(file_name)
1909
1910    def addNonSliceableExtension(self, extension):
1911        self._non_sliceable_extensions.append(extension)
1912
1913    @pyqtSlot(str, result=bool)
1914    def checkIsValidProjectFile(self, file_url):
1915        """Checks if the given file URL is a valid project file. """
1916
1917        file_path = QUrl(file_url).toLocalFile()
1918        workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
1919        if workspace_reader is None:
1920            return False  # non-project files won't get a reader
1921        try:
1922            result = workspace_reader.preRead(file_path, show_dialog=False)
1923            return result == WorkspaceReader.PreReadResult.accepted
1924        except Exception:
1925            Logger.logException("e", "Could not check file %s", file_url)
1926            return False
1927
1928    def _onContextMenuRequested(self, x: float, y: float) -> None:
1929        # Ensure we select the object if we request a context menu over an object without having a selection.
1930        if Selection.hasSelection():
1931            return
1932        selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection"))
1933        if not selection_pass:  # If you right-click before the rendering has been initialised there might not be a selection pass yet.
1934            return
1935        node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
1936        if not node:
1937            return
1938        parent = node.getParent()
1939        while parent and parent.callDecoration("isGroup"):
1940            node = parent
1941            parent = node.getParent()
1942
1943        Selection.add(node)
1944
1945    @pyqtSlot()
1946    def showMoreInformationDialogForAnonymousDataCollection(self):
1947        try:
1948            slice_info = self._plugin_registry.getPluginObject("SliceInfoPlugin")
1949            slice_info.showMoreInfoDialog()
1950        except PluginNotFoundError:
1951            Logger.log("w", "Plugin SliceInfo was not found, so not able to show the info dialog.")
1952
1953    def addSidebarCustomMenuItem(self, menu_item: dict) -> None:
1954        self._sidebar_custom_menu_items.append(menu_item)
1955
1956    def getSidebarCustomMenuItems(self) -> list:
1957        return self._sidebar_custom_menu_items
1958
1959    @pyqtSlot(result = bool)
1960    def shouldShowWelcomeDialog(self) -> bool:
1961        # Only show the complete flow if there is no printer yet.
1962        return self._machine_manager.activeMachine is None
1963
1964    @pyqtSlot(result = bool)
1965    def shouldShowWhatsNewDialog(self) -> bool:
1966        has_active_machine = self._machine_manager.activeMachine is not None
1967        has_app_just_upgraded = self.hasJustUpdatedFromOldVersion()
1968
1969        # Only show the what's new dialog if there's no machine and we have just upgraded
1970        show_whatsnew_only = has_active_machine and has_app_just_upgraded
1971        return show_whatsnew_only
1972
1973    @pyqtSlot(result = int)
1974    def appWidth(self) -> int:
1975        main_window = QtApplication.getInstance().getMainWindow()
1976        if main_window:
1977            return main_window.width()
1978        return 0
1979
1980    @pyqtSlot(result = int)
1981    def appHeight(self) -> int:
1982        main_window = QtApplication.getInstance().getMainWindow()
1983        if main_window:
1984            return main_window.height()
1985        return 0
1986
1987    @pyqtSlot()
1988    def deleteAll(self, only_selectable: bool = True) -> None:
1989        super().deleteAll(only_selectable = only_selectable)
1990
1991        # Also remove nodes with LayerData
1992        self._removeNodesWithLayerData(only_selectable = only_selectable)
1993
1994    def _removeNodesWithLayerData(self, only_selectable: bool = True) -> None:
1995        Logger.log("i", "Clearing scene")
1996        nodes = []
1997        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
1998            if not isinstance(node, SceneNode):
1999                continue
2000            if not node.isEnabled():
2001                continue
2002            if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
2003                continue  # Node that doesnt have a mesh and is not a group.
2004            if only_selectable and not node.isSelectable():
2005                continue  # Only remove nodes that are selectable.
2006            if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
2007                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
2008            nodes.append(node)
2009        if nodes:
2010            from UM.Operations.GroupedOperation import GroupedOperation
2011            op = GroupedOperation()
2012
2013            for node in nodes:
2014                from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
2015                op.addOperation(RemoveSceneNodeOperation(node))
2016
2017                # Reset the print information
2018                self.getController().getScene().sceneChanged.emit(node)
2019
2020            op.push()
2021            from UM.Scene.Selection import Selection
2022            Selection.clear()
2023
2024    @classmethod
2025    def getInstance(cls, *args, **kwargs) -> "CuraApplication":
2026        return cast(CuraApplication, super().getInstance(**kwargs))
2027