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