1# Copyright (c) 2018 Ultimaker B.V. 2# Uranium is released under the terms of the LGPLv3 or higher. 3 4import argparse 5import os 6import sys 7import threading 8 9from UM.Controller import Controller 10from UM.Message import Message #For typing. 11from UM.PackageManager import PackageManager 12from UM.PluginRegistry import PluginRegistry 13from UM.Resources import Resources 14from UM.Operations.OperationStack import OperationStack 15from UM.Event import CallFunctionEvent 16import UM.Settings 17import UM.Settings.ContainerStack 18import UM.Settings.InstanceContainer 19from UM.Settings.ContainerRegistry import ContainerRegistry 20from UM.Signal import Signal, signalemitter 21from UM.Logger import Logger 22from UM.Preferences import Preferences 23from UM.View.Renderer import Renderer #For typing. 24from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager 25from UM.Workspace.WorkspaceMetadataStorage import WorkspaceMetadataStorage 26from UM.i18n import i18nCatalog 27from UM.Version import Version 28 29from typing import TYPE_CHECKING, List, Callable, Any, Optional 30if TYPE_CHECKING: 31 from UM.Backend.Backend import Backend 32 from UM.Settings.ContainerStack import ContainerStack 33 from UM.Extension import Extension 34 35 36@signalemitter 37class Application: 38 """Central object responsible for running the main event loop and creating other central objects. 39 40 The Application object is a central object for accessing other important objects. It is also 41 responsible for starting the main event loop. It is passed on to plugins so it can be easily 42 used to access objects required for those plugins. 43 """ 44 45 def __init__(self, name: str, version: str, api_version: str, app_display_name: str = "", build_type: str = "", is_debug_mode: bool = False, **kwargs) -> None: 46 """Init method 47 48 :param name: :type{string} The name of the application. 49 :param version: :type{string} Version, formatted as major.minor.rev 50 :param build_type: Additional version info on the type of build this is, such as "master". 51 :param is_debug_mode: Whether to run in debug mode. 52 """ 53 54 if Application.__instance is not None: 55 raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) 56 Application.__instance = self 57 58 super().__init__() # Call super to make multiple inheritance work. 59 60 self._api_version = Version(api_version) # type: Version 61 62 self._app_name = name # type: str 63 self._app_display_name = app_display_name if app_display_name else name # type: str 64 self._version = version # type: str 65 self._build_type = build_type # type: str 66 self._is_debug_mode = is_debug_mode # type: bool 67 self._is_headless = False # type: bool 68 self._use_external_backend = False # type: bool 69 70 self._just_updated_from_old_version = False # type: bool 71 72 self._config_lock_filename = "{name}.lock".format(name = self._app_name) # type: str 73 74 self._cli_args = None # type: argparse.Namespace 75 self._cli_parser = argparse.ArgumentParser(prog = self._app_name, add_help = False) # type: argparse.ArgumentParser 76 77 self._main_thread = threading.current_thread() # type: threading.Thread 78 79 self.default_theme = self._app_name # type: str # Default theme is the application name 80 self._default_language = "en_US" # type: str 81 82 self.change_log_url = "https://github.com/Ultimaker/Uranium" # Where to find a more detailed description of the recent updates. 83 84 self._preferences_filename = None # type: str 85 self._preferences = None # type: Preferences 86 87 self._extensions = [] # type: List[Extension] 88 self._required_plugins = [] # type: List[str] 89 90 self._package_manager_class = PackageManager # type: type 91 self._package_manager = None # type: PackageManager 92 93 self._plugin_registry = None # type: PluginRegistry 94 self._container_registry_class = ContainerRegistry # type: type 95 self._container_registry = None # type: ContainerRegistry 96 self._global_container_stack = None # type: Optional[ContainerStack] 97 98 self._controller = None # type: Controller 99 self._backend = None # type: Backend 100 self._output_device_manager = None # type: OutputDeviceManager 101 self._operation_stack = None # type: OperationStack 102 103 self._visible_messages = [] # type: List[Message] 104 self._message_lock = threading.Lock() # type: threading.Lock 105 106 self._app_install_dir = self.getInstallPrefix() # type: str 107 108 self._workspace_metadata_storage = WorkspaceMetadataStorage() # type: WorkspaceMetadataStorage 109 110 def getAPIVersion(self) -> "Version": 111 return self._api_version 112 113 def getWorkspaceMetadataStorage(self) -> WorkspaceMetadataStorage: 114 return self._workspace_metadata_storage 115 116 # Adds the command line options that can be parsed by the command line parser. 117 # Can be overridden to add additional command line options to the parser. 118 def addCommandLineOptions(self) -> None: 119 self._cli_parser.add_argument("--version", 120 action = "version", 121 version = "%(prog)s version: {0}".format(self._version)) 122 self._cli_parser.add_argument("--external-backend", 123 action = "store_true", 124 default = False, 125 help = "Use an externally started backend instead of starting it automatically. This is a debug feature to make it possible to run the engine with debug options enabled.") 126 self._cli_parser.add_argument('--headless', 127 action = 'store_true', 128 default = False, 129 help = "Hides all GUI elements.") 130 self._cli_parser.add_argument("--debug", 131 action = "store_true", 132 default = False, 133 help = "Turn on the debug mode by setting this option.") 134 135 def parseCliOptions(self) -> None: 136 self._cli_args = self._cli_parser.parse_args() 137 138 self._is_headless = self._cli_args.headless 139 self._is_debug_mode = self._cli_args.debug or self._is_debug_mode 140 self._use_external_backend = self._cli_args.external_backend 141 142 # Performs initialization that must be done before start. 143 def initialize(self) -> None: 144 Logger.log("d", "Initializing %s", self._app_display_name) 145 Logger.log("d", "App Version %s", self._version) 146 Logger.log("d", "Api Version %s", self._api_version) 147 Logger.log("d", "Build type %s", self._build_type or "None") 148 # For Ubuntu Unity this makes Qt use its own menu bar rather than pass it on to Unity. 149 os.putenv("UBUNTU_MENUPROXY", "0") 150 151 # Custom signal handling 152 Signal._app = self 153 Signal._signalQueue = self 154 155 # Initialize Resources. Set the application name and version here because we can only know the actual info 156 # after the __init__() has been called. 157 Resources.ApplicationIdentifier = self._app_name 158 Resources.ApplicationVersion = self._version 159 160 app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable))) 161 Resources.addSearchPath(os.path.join(app_root, "share", "uranium", "resources")) 162 163 Resources.addSearchPath(os.path.join(os.path.dirname(sys.executable), "resources")) 164 Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "uranium", "resources")) 165 Resources.addSearchPath(os.path.join(self._app_install_dir, "Resources", "uranium", "resources")) 166 Resources.addSearchPath(os.path.join(self._app_install_dir, "Resources", self._app_name, "resources")) 167 168 if not hasattr(sys, "frozen"): 169 Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")) 170 171 i18nCatalog.setApplication(self) 172 173 PluginRegistry.addType("backend", self.setBackend) 174 PluginRegistry.addType("logger", Logger.addLogger) 175 PluginRegistry.addType("extension", self.addExtension) 176 177 self._preferences = Preferences() 178 self._preferences.addPreference("general/language", self._default_language) 179 self._preferences.addPreference("general/visible_settings", "") 180 self._preferences.addPreference("general/plugins_to_remove", "") 181 self._preferences.addPreference("general/disabled_plugins", "") 182 183 self._controller = Controller(self) 184 self._output_device_manager = OutputDeviceManager() 185 186 self._operation_stack = OperationStack(self._controller) 187 188 self._plugin_registry = PluginRegistry(self) 189 190 self._plugin_registry.addPluginLocation(os.path.join(app_root, "share", "uranium", "plugins")) 191 self._plugin_registry.addPluginLocation(os.path.join(app_root, "share", "cura", "plugins")) 192 193 self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "lib", "uranium")) 194 self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "lib64", "uranium")) 195 self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "lib32", "uranium")) 196 self._plugin_registry.addPluginLocation(os.path.join(os.path.dirname(sys.executable), "plugins")) 197 self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "Resources", "uranium", "plugins")) 198 self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "Resources", self._app_name, "plugins")) 199 # Locally installed plugins 200 local_path = os.path.join(Resources.getStoragePath(Resources.Resources), "plugins") 201 # Ensure the local plugins directory exists 202 try: 203 os.makedirs(local_path) 204 except OSError: 205 pass 206 self._plugin_registry.addPluginLocation(local_path) 207 208 if not hasattr(sys, "frozen"): 209 self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins")) 210 211 self._container_registry = self._container_registry_class(self) 212 213 UM.Settings.InstanceContainer.setContainerRegistry(self._container_registry) 214 UM.Settings.ContainerStack.setContainerRegistry(self._container_registry) 215 216 self.showMessageSignal.connect(self.showMessage) 217 self.hideMessageSignal.connect(self.hideMessage) 218 219 def startSplashWindowPhase(self) -> None: 220 pass 221 222 def startPostSplashWindowPhase(self) -> None: 223 pass 224 225 # Indicates if we have just updated from an older application version. 226 def hasJustUpdatedFromOldVersion(self) -> bool: 227 return self._just_updated_from_old_version 228 229 def run(self): 230 """Run the main event loop. 231 This method should be re-implemented by subclasses to start the main event loop. 232 :exception NotImplementedError: 233 """ 234 235 self.addCommandLineOptions() 236 self.parseCliOptions() 237 self.initialize() 238 239 self.startSplashWindowPhase() 240 self.startPostSplashWindowPhase() 241 242 def getContainerRegistry(self) -> ContainerRegistry: 243 return self._container_registry 244 245 def getApplicationLockFilename(self) -> str: 246 """Get the lock filename""" 247 248 return self._config_lock_filename 249 250 applicationShuttingDown = Signal() 251 """Emitted when the application window was closed and we need to shut down the application""" 252 253 showMessageSignal = Signal() 254 255 hideMessageSignal = Signal() 256 257 globalContainerStackChanged = Signal() 258 259 workspaceLoaded = Signal() 260 261 def setGlobalContainerStack(self, stack: Optional["ContainerStack"]) -> None: 262 if self._global_container_stack != stack: 263 self._global_container_stack = stack 264 self.globalContainerStackChanged.emit() 265 266 def getGlobalContainerStack(self) -> Optional["ContainerStack"]: 267 return self._global_container_stack 268 269 def hideMessage(self, message: Message) -> None: 270 raise NotImplementedError 271 272 def showMessage(self, message: Message) -> None: 273 raise NotImplementedError 274 275 def showToastMessage(self, title: str, message: str) -> None: 276 raise NotImplementedError 277 278 def getVersion(self) -> str: 279 """Get the version of the application""" 280 281 return self._version 282 283 def getBuildType(self) -> str: 284 """Get the build type of the application""" 285 286 return self._build_type 287 288 def getIsDebugMode(self) -> bool: 289 return self._is_debug_mode 290 291 def getIsHeadLess(self) -> bool: 292 return self._is_headless 293 294 def getUseExternalBackend(self) -> bool: 295 return self._use_external_backend 296 297 visibleMessageAdded = Signal() 298 299 def hideMessageById(self, message_id: int) -> None: 300 """Hide message by ID (as provided by built-in id function)""" 301 302 # If a user and the application tries to close same message dialog simultaneously, message_id could become an empty 303 # string, and then the application will raise an error when trying to do "int(message_id)". 304 # So we check the message_id here. 305 if not message_id: 306 return 307 308 found_message = None 309 with self._message_lock: 310 for message in self._visible_messages: 311 if id(message) == int(message_id): 312 found_message = message 313 if found_message is not None: 314 self.hideMessageSignal.emit(found_message) 315 316 visibleMessageRemoved = Signal() 317 318 def getVisibleMessages(self) -> List[Message]: 319 """Get list of all visible messages""" 320 321 with self._message_lock: 322 return self._visible_messages 323 324 def _loadPlugins(self) -> None: 325 """Function that needs to be overridden by child classes with a list of plugins it needs.""" 326 327 pass 328 329 def getApplicationName(self) -> str: 330 """Get name of the application. 331 :returns: app_name 332 """ 333 334 return self._app_name 335 336 def getApplicationDisplayName(self) -> str: 337 return self._app_display_name 338 339 def getPreferences(self) -> Preferences: 340 """Get the preferences. 341 :return: preferences 342 """ 343 344 return self._preferences 345 346 def savePreferences(self) -> None: 347 if self._preferences_filename: 348 self._preferences.writeToFile(self._preferences_filename) 349 else: 350 Logger.log("i", "Preferences filename not set. Unable to save file.") 351 352 def getApplicationLanguage(self) -> str: 353 """Get the currently used IETF language tag. 354 The returned tag is during runtime used to translate strings. 355 :returns: Language tag. 356 """ 357 358 language = os.getenv("URANIUM_LANGUAGE") 359 if not language: 360 language = self._preferences.getValue("general/language") 361 if not language: 362 language = os.getenv("LANGUAGE") 363 if not language: 364 language = self._default_language 365 366 return language 367 368 def getRequiredPlugins(self) -> List[str]: 369 """Application has a list of plugins that it *must* have. If it does not have these, it cannot function. 370 These plugins can not be disabled in any way. 371 """ 372 373 return self._required_plugins 374 375 def setRequiredPlugins(self, plugin_names: List[str]) -> None: 376 """Set the plugins that the application *must* have in order to function. 377 :param plugin_names: List of strings with the names of the required plugins 378 """ 379 380 self._required_plugins = plugin_names 381 382 def setBackend(self, backend: "Backend") -> None: 383 """Set the backend of the application (the program that does the heavy lifting).""" 384 385 self._backend = backend 386 387 def getBackend(self) -> "Backend": 388 """Get the backend of the application (the program that does the heavy lifting). 389 :returns: Backend 390 """ 391 392 return self._backend 393 394 def getPluginRegistry(self) -> PluginRegistry: 395 """Get the PluginRegistry of this application. 396 :returns: PluginRegistry 397 """ 398 399 return self._plugin_registry 400 401 def getController(self) -> Controller: 402 """Get the Controller of this application. 403 :returns: Controller 404 """ 405 406 return self._controller 407 408 def getOperationStack(self) -> OperationStack: 409 return self._operation_stack 410 411 def getOutputDeviceManager(self) -> OutputDeviceManager: 412 return self._output_device_manager 413 414 def getRenderer(self) -> Renderer: 415 """Return an application-specific Renderer object. 416 :exception NotImplementedError 417 """ 418 419 raise NotImplementedError("getRenderer must be implemented by subclasses.") 420 421 def functionEvent(self, event: CallFunctionEvent) -> None: 422 """Post a function event onto the event loop. 423 424 This takes a CallFunctionEvent object and puts it into the actual event loop. 425 :exception NotImplementedError 426 """ 427 428 raise NotImplementedError("functionEvent must be implemented by subclasses.") 429 430 def callLater(self, func: Callable[..., Any], *args, **kwargs) -> None: 431 """Call a function the next time the event loop runs. 432 433 You can't get the result of this function directly. It won't block. 434 :param func: The function to call. 435 :param args: The positional arguments to pass to the function. 436 :param kwargs: The keyword arguments to pass to the function. 437 """ 438 439 event = CallFunctionEvent(func, args, kwargs) 440 self.functionEvent(event) 441 442 def getMainThread(self) -> threading.Thread: 443 """Get the application's main thread.""" 444 445 return self._main_thread 446 447 def addExtension(self, extension: "Extension") -> None: 448 self._extensions.append(extension) 449 450 def getExtensions(self) -> List["Extension"]: 451 return self._extensions 452 453 # Returns the path to the folder of the app itself, e.g.: '/root/blah/programs/Cura'. 454 @staticmethod 455 def getAppFolderPrefix() -> str: 456 if "python" in os.path.basename(sys.executable): 457 executable = sys.argv[0] 458 else: 459 executable = sys.executable 460 return os.path.dirname(os.path.realpath(executable)) 461 462 # Returns the path to the folder the app is installed _in_, e.g.: '/root/blah/programs' 463 @staticmethod 464 def getInstallPrefix() -> str: 465 return os.path.abspath(os.path.join(Application.getAppFolderPrefix(), "..")) 466 467 __instance = None # type: Application 468 469 @classmethod 470 def getInstance(cls, *args, **kwargs) -> "Application": 471 return cls.__instance 472