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