Browse Source

Merge branch 'master' into feature_firmware_updater

fieldOfView 6 years ago
parent
commit
c4919d65f4

+ 117 - 0
cura/API/Account.py

@@ -0,0 +1,117 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional, Dict, TYPE_CHECKING
+
+from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
+
+from UM.i18n import i18nCatalog
+from UM.Message import Message
+
+from cura.OAuth2.AuthorizationService import AuthorizationService
+from cura.OAuth2.Models import OAuth2Settings
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
+i18n_catalog = i18nCatalog("cura")
+
+
+##  The account API provides a version-proof bridge to use Ultimaker Accounts
+#
+#   Usage:
+#       ``from cura.API import CuraAPI
+#       api = CuraAPI()
+#       api.account.login()
+#       api.account.logout()
+#       api.account.userProfile # Who is logged in``
+#
+class Account(QObject):
+    # Signal emitted when user logged in or out.
+    loginStateChanged = pyqtSignal(bool)
+
+    def __init__(self, application: "CuraApplication", parent = None) -> None:
+        super().__init__(parent)
+        self._application = application
+
+        self._error_message = None  # type: Optional[Message]
+        self._logged_in = False
+
+        self._callback_port = 32118
+        self._oauth_root = "https://account.ultimaker.com"
+        self._cloud_api_root = "https://api.ultimaker.com"
+
+        self._oauth_settings = OAuth2Settings(
+            OAUTH_SERVER_URL= self._oauth_root,
+            CALLBACK_PORT=self._callback_port,
+            CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
+            CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
+            CLIENT_SCOPES="user.read drive.backups.read drive.backups.write",
+            AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
+            AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
+            AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
+        )
+
+        self._authorization_service = AuthorizationService(self._oauth_settings)
+
+    def initialize(self) -> None:
+        self._authorization_service.initialize(self._application.getPreferences())
+
+        self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
+        self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
+        self._authorization_service.loadAuthDataFromPreferences()
+
+    @pyqtProperty(bool, notify=loginStateChanged)
+    def isLoggedIn(self) -> bool:
+        return self._logged_in
+
+    def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
+        if error_message:
+            if self._error_message:
+                self._error_message.hide()
+            self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
+            self._error_message.show()
+
+        if self._logged_in != logged_in:
+            self._logged_in = logged_in
+            self.loginStateChanged.emit(logged_in)
+
+    @pyqtSlot()
+    def login(self) -> None:
+        if self._logged_in:
+            # Nothing to do, user already logged in.
+            return
+        self._authorization_service.startAuthorizationFlow()
+
+    @pyqtProperty(str, notify=loginStateChanged)
+    def userName(self):
+        user_profile = self._authorization_service.getUserProfile()
+        if not user_profile:
+            return None
+        return user_profile.username
+
+    @pyqtProperty(str, notify = loginStateChanged)
+    def profileImageUrl(self):
+        user_profile = self._authorization_service.getUserProfile()
+        if not user_profile:
+            return None
+        return user_profile.profile_image_url
+
+    @pyqtProperty(str, notify=loginStateChanged)
+    def accessToken(self) -> Optional[str]:
+        return self._authorization_service.getAccessToken()
+
+    #   Get the profile of the logged in user
+    #   @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
+    @pyqtProperty("QVariantMap", notify = loginStateChanged)
+    def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
+        user_profile = self._authorization_service.getUserProfile()
+        if not user_profile:
+            return None
+        return user_profile.__dict__
+
+    @pyqtSlot()
+    def logout(self) -> None:
+        if not self._logged_in:
+            return  # Nothing to do, user isn't logged in.
+
+        self._authorization_service.deleteAuthData()

+ 7 - 3
cura/API/Backups.py

@@ -1,9 +1,12 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
-from typing import Tuple, Optional
+from typing import Tuple, Optional, TYPE_CHECKING
 
 
 from cura.Backups.BackupsManager import BackupsManager
 from cura.Backups.BackupsManager import BackupsManager
 
 
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
 
 
 ##  The back-ups API provides a version-proof bridge between Cura's
 ##  The back-ups API provides a version-proof bridge between Cura's
 #   BackupManager and plug-ins that hook into it.
 #   BackupManager and plug-ins that hook into it.
@@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager
 #       api = CuraAPI()
 #       api = CuraAPI()
 #       api.backups.createBackup()
 #       api.backups.createBackup()
 #       api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
 #       api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
-
 class Backups:
 class Backups:
-    manager = BackupsManager()  # Re-used instance of the backups manager.
+
+    def __init__(self, application: "CuraApplication") -> None:
+        self.manager = BackupsManager(application)
 
 
     ##  Create a new back-up using the BackupsManager.
     ##  Create a new back-up using the BackupsManager.
     #   \return Tuple containing a ZIP file with the back-up data and a dict
     #   \return Tuple containing a ZIP file with the back-up data and a dict

+ 9 - 4
cura/API/Interface/Settings.py

@@ -1,7 +1,11 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-from cura.CuraApplication import CuraApplication
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
 
 
 ##  The Interface.Settings API provides a version-proof bridge between Cura's
 ##  The Interface.Settings API provides a version-proof bridge between Cura's
 #   (currently) sidebar UI and plug-ins that hook into it.
 #   (currently) sidebar UI and plug-ins that hook into it.
@@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication
 #       api.interface.settings.addContextMenuItem(data)``
 #       api.interface.settings.addContextMenuItem(data)``
 
 
 class Settings:
 class Settings:
-    # Re-used instance of Cura:
-    application = CuraApplication.getInstance()  # type: CuraApplication
+
+    def __init__(self, application: "CuraApplication") -> None:
+        self.application = application
 
 
     ##  Add items to the sidebar context menu.
     ##  Add items to the sidebar context menu.
     #   \param menu_item dict containing the menu item to add.
     #   \param menu_item dict containing the menu item to add.
@@ -30,4 +35,4 @@ class Settings:
     ##  Get all custom items currently added to the sidebar context menu.
     ##  Get all custom items currently added to the sidebar context menu.
     #   \return List containing all custom context menu items.
     #   \return List containing all custom context menu items.
     def getContextMenuItems(self) -> list:
     def getContextMenuItems(self) -> list:
-        return self.application.getSidebarCustomMenuItems()
+        return self.application.getSidebarCustomMenuItems()

+ 9 - 2
cura/API/Interface/__init__.py

@@ -1,9 +1,15 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
+from typing import TYPE_CHECKING
+
 from UM.PluginRegistry import PluginRegistry
 from UM.PluginRegistry import PluginRegistry
 from cura.API.Interface.Settings import Settings
 from cura.API.Interface.Settings import Settings
 
 
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
+
 ##  The Interface class serves as a common root for the specific API
 ##  The Interface class serves as a common root for the specific API
 #   methods for each interface element.
 #   methods for each interface element.
 #
 #
@@ -20,5 +26,6 @@ class Interface:
     # For now we use the same API version to be consistent.
     # For now we use the same API version to be consistent.
     VERSION = PluginRegistry.APIVersion
     VERSION = PluginRegistry.APIVersion
 
 
-    # API methods specific to the settings portion of the UI
-    settings = Settings()
+    def __init__(self, application: "CuraApplication") -> None:
+        # API methods specific to the settings portion of the UI
+        self.settings = Settings(application)

+ 48 - 6
cura/API/__init__.py

@@ -1,8 +1,17 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional, TYPE_CHECKING
+
+from PyQt5.QtCore import QObject, pyqtProperty
+
 from UM.PluginRegistry import PluginRegistry
 from UM.PluginRegistry import PluginRegistry
 from cura.API.Backups import Backups
 from cura.API.Backups import Backups
 from cura.API.Interface import Interface
 from cura.API.Interface import Interface
+from cura.API.Account import Account
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
 
 
 ##  The official Cura API that plug-ins can use to interact with Cura.
 ##  The official Cura API that plug-ins can use to interact with Cura.
 #
 #
@@ -10,14 +19,47 @@ from cura.API.Interface import Interface
 #   this API provides a version-safe interface with proper deprecation warnings
 #   this API provides a version-safe interface with proper deprecation warnings
 #   etc. Usage of any other methods than the ones provided in this API can cause
 #   etc. Usage of any other methods than the ones provided in this API can cause
 #   plug-ins to be unstable.
 #   plug-ins to be unstable.
-
-class CuraAPI:
+class CuraAPI(QObject):
 
 
     # For now we use the same API version to be consistent.
     # For now we use the same API version to be consistent.
     VERSION = PluginRegistry.APIVersion
     VERSION = PluginRegistry.APIVersion
+    __instance = None  # type: "CuraAPI"
+    _application = None  # type: CuraApplication
+
+    #   This is done to ensure that the first time an instance is created, it's forced that the application is set.
+    #   The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
+    #   Since the API is intended to be used by plugins, the cura application should have already created this.
+    def __new__(cls, application: Optional["CuraApplication"] = None):
+        if cls.__instance is None:
+            if application is None:
+                raise Exception("Upon first time creation, the application must be set.")
+            cls.__instance = super(CuraAPI, cls).__new__(cls)
+            cls._application = application
+        return cls.__instance
+
+    def __init__(self, application: Optional["CuraApplication"] = None) -> None:
+        super().__init__(parent = CuraAPI._application)
+
+        # Accounts API
+        self._account = Account(self._application)
+
+        # Backups API
+        self._backups = Backups(self._application)
+
+        # Interface API
+        self._interface = Interface(self._application)
+
+    def initialize(self) -> None:
+        self._account.initialize()
+
+    @pyqtProperty(QObject, constant = True)
+    def account(self) -> "Account":
+        return self._account
 
 
-    # Backups API
-    backups = Backups()
+    @property
+    def backups(self) -> "Backups":
+        return self._backups
 
 
-    # Interface API
-    interface = Interface()
+    @property
+    def interface(self) -> "Interface":
+        return self._interface

+ 12 - 11
cura/Backups/Backup.py

@@ -4,18 +4,18 @@
 import io
 import io
 import os
 import os
 import re
 import re
-
 import shutil
 import shutil
-
-from typing import Dict, Optional
 from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
 from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
+from typing import Dict, Optional, TYPE_CHECKING
 
 
 from UM import i18nCatalog
 from UM import i18nCatalog
 from UM.Logger import Logger
 from UM.Logger import Logger
 from UM.Message import Message
 from UM.Message import Message
 from UM.Platform import Platform
 from UM.Platform import Platform
 from UM.Resources import Resources
 from UM.Resources import Resources
-from cura.CuraApplication import CuraApplication
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
 
 
 
 
 ##  The back-up class holds all data about a back-up.
 ##  The back-up class holds all data about a back-up.
@@ -29,24 +29,25 @@ class Backup:
     # Re-use translation catalog.
     # Re-use translation catalog.
     catalog = i18nCatalog("cura")
     catalog = i18nCatalog("cura")
 
 
-    def __init__(self, zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
+    def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
+        self._application = application
         self.zip_file = zip_file  # type: Optional[bytes]
         self.zip_file = zip_file  # type: Optional[bytes]
         self.meta_data = meta_data  # type: Optional[Dict[str, str]]
         self.meta_data = meta_data  # type: Optional[Dict[str, str]]
 
 
     ##  Create a back-up from the current user config folder.
     ##  Create a back-up from the current user config folder.
     def makeFromCurrent(self) -> None:
     def makeFromCurrent(self) -> None:
-        cura_release = CuraApplication.getInstance().getVersion()
+        cura_release = self._application.getVersion()
         version_data_dir = Resources.getDataStoragePath()
         version_data_dir = Resources.getDataStoragePath()
 
 
         Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
         Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
 
 
         # Ensure all current settings are saved.
         # Ensure all current settings are saved.
-        CuraApplication.getInstance().saveSettings()
+        self._application.saveSettings()
 
 
         # We copy the preferences file to the user data directory in Linux as it's in a different location there.
         # We copy the preferences file to the user data directory in Linux as it's in a different location there.
         # When restoring a backup on Linux, we move it back.
         # When restoring a backup on Linux, we move it back.
         if Platform.isLinux():
         if Platform.isLinux():
-            preferences_file_name = CuraApplication.getInstance().getApplicationName()
+            preferences_file_name = self._application.getApplicationName()
             preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
             preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
             backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
             backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
             Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
             Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
@@ -58,7 +59,7 @@ class Backup:
         if archive is None:
         if archive is None:
             return
             return
         files = archive.namelist()
         files = archive.namelist()
-        
+
         # Count the metadata items. We do this in a rather naive way at the moment.
         # Count the metadata items. We do this in a rather naive way at the moment.
         machine_count = len([s for s in files if "machine_instances/" in s]) - 1
         machine_count = len([s for s in files if "machine_instances/" in s]) - 1
         material_count = len([s for s in files if "materials/" in s]) - 1
         material_count = len([s for s in files if "materials/" in s]) - 1
@@ -112,7 +113,7 @@ class Backup:
                                    "Tried to restore a Cura backup without having proper data or meta data."))
                                    "Tried to restore a Cura backup without having proper data or meta data."))
             return False
             return False
 
 
-        current_version = CuraApplication.getInstance().getVersion()
+        current_version = self._application.getVersion()
         version_to_restore = self.meta_data.get("cura_release", "master")
         version_to_restore = self.meta_data.get("cura_release", "master")
         if current_version != version_to_restore:
         if current_version != version_to_restore:
             # Cannot restore version older or newer than current because settings might have changed.
             # Cannot restore version older or newer than current because settings might have changed.
@@ -128,7 +129,7 @@ class Backup:
 
 
         # Under Linux, preferences are stored elsewhere, so we copy the file to there.
         # Under Linux, preferences are stored elsewhere, so we copy the file to there.
         if Platform.isLinux():
         if Platform.isLinux():
-            preferences_file_name = CuraApplication.getInstance().getApplicationName()
+            preferences_file_name = self._application.getApplicationName()
             preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
             preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
             backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
             backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
             Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
             Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)

+ 8 - 6
cura/Backups/BackupsManager.py

@@ -1,11 +1,13 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-from typing import Dict, Optional, Tuple
+from typing import Dict, Optional, Tuple, TYPE_CHECKING
 
 
 from UM.Logger import Logger
 from UM.Logger import Logger
 from cura.Backups.Backup import Backup
 from cura.Backups.Backup import Backup
-from cura.CuraApplication import CuraApplication
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
 
 
 
 
 ##  The BackupsManager is responsible for managing the creating and restoring of
 ##  The BackupsManager is responsible for managing the creating and restoring of
@@ -13,15 +15,15 @@ from cura.CuraApplication import CuraApplication
 #
 #
 #   Back-ups themselves are represented in a different class.
 #   Back-ups themselves are represented in a different class.
 class BackupsManager:
 class BackupsManager:
-    def __init__(self):
-        self._application = CuraApplication.getInstance()
+    def __init__(self, application: "CuraApplication") -> None:
+        self._application = application
 
 
     ##  Get a back-up of the current configuration.
     ##  Get a back-up of the current configuration.
     #   \return A tuple containing a ZipFile (the actual back-up) and a dict
     #   \return A tuple containing a ZipFile (the actual back-up) and a dict
     #   containing some metadata (like version).
     #   containing some metadata (like version).
     def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
     def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
         self._disableAutoSave()
         self._disableAutoSave()
-        backup = Backup()
+        backup = Backup(self._application)
         backup.makeFromCurrent()
         backup.makeFromCurrent()
         self._enableAutoSave()
         self._enableAutoSave()
         # We don't return a Backup here because we want plugins only to interact with our API and not full objects.
         # We don't return a Backup here because we want plugins only to interact with our API and not full objects.
@@ -39,7 +41,7 @@ class BackupsManager:
 
 
         self._disableAutoSave()
         self._disableAutoSave()
 
 
-        backup = Backup(zip_file = zip_file, meta_data = meta_data)
+        backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
         restored = backup.restore()
         restored = backup.restore()
         if restored:
         if restored:
             # At this point, Cura will need to restart for the changes to take effect.
             # At this point, Cura will need to restart for the changes to take effect.

+ 18 - 1
cura/CuraApplication.py

@@ -44,6 +44,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
 from UM.Operations.GroupedOperation import GroupedOperation
 from UM.Operations.GroupedOperation import GroupedOperation
 from UM.Operations.SetTransformOperation import SetTransformOperation
 from UM.Operations.SetTransformOperation import SetTransformOperation
 
 
+from cura.API import CuraAPI
 from cura.Arranging.Arrange import Arrange
 from cura.Arranging.Arrange import Arrange
 from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
 from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
 from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
 from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
@@ -61,6 +62,7 @@ from cura.Scene.CuraSceneController import CuraSceneController
 from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
 from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
 from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.Settings.SettingFunction import SettingFunction
 from UM.Settings.SettingFunction import SettingFunction
+from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
 from cura.Settings.MachineNameValidator import MachineNameValidator
 from cura.Settings.MachineNameValidator import MachineNameValidator
 
 
 from cura.Machines.Models.BuildPlateModel import BuildPlateModel
 from cura.Machines.Models.BuildPlateModel import BuildPlateModel
@@ -203,6 +205,7 @@ class CuraApplication(QtApplication):
 
 
         self._quality_profile_drop_down_menu_model = None
         self._quality_profile_drop_down_menu_model = None
         self._custom_quality_profile_drop_down_menu_model = None
         self._custom_quality_profile_drop_down_menu_model = None
+        self._cura_API = CuraAPI(self)
 
 
         self._physics = None
         self._physics = None
         self._volume = None
         self._volume = None
@@ -241,6 +244,8 @@ class CuraApplication(QtApplication):
 
 
         from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
         from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
         self._container_registry_class = CuraContainerRegistry
         self._container_registry_class = CuraContainerRegistry
+        # Redefined here in order to please the typing.
+        self._container_registry = None # type: CuraContainerRegistry
         from cura.CuraPackageManager import CuraPackageManager
         from cura.CuraPackageManager import CuraPackageManager
         self._package_manager_class = CuraPackageManager
         self._package_manager_class = CuraPackageManager
 
 
@@ -265,6 +270,9 @@ class CuraApplication(QtApplication):
                                       help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
                                       help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
         self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
         self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
 
 
+    def getContainerRegistry(self) -> "CuraContainerRegistry":
+        return self._container_registry
+
     def parseCliOptions(self):
     def parseCliOptions(self):
         super().parseCliOptions()
         super().parseCliOptions()
 
 
@@ -674,7 +682,7 @@ class CuraApplication(QtApplication):
 
 
         Logger.log("i", "Initializing quality manager")
         Logger.log("i", "Initializing quality manager")
         from cura.Machines.QualityManager import QualityManager
         from cura.Machines.QualityManager import QualityManager
-        self._quality_manager = QualityManager(container_registry, parent = self)
+        self._quality_manager = QualityManager(self, parent = self)
         self._quality_manager.initialize()
         self._quality_manager.initialize()
 
 
         Logger.log("i", "Initializing machine manager")
         Logger.log("i", "Initializing machine manager")
@@ -706,6 +714,9 @@ class CuraApplication(QtApplication):
         default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
         default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
         self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
         self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
 
 
+        # Initialize Cura API
+        self._cura_API.initialize()
+
         # Detect in which mode to run and execute that mode
         # Detect in which mode to run and execute that mode
         if self._is_headless:
         if self._is_headless:
             self.runWithoutGUI()
             self.runWithoutGUI()
@@ -893,6 +904,9 @@ class CuraApplication(QtApplication):
             self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
             self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
         return self._custom_quality_profile_drop_down_menu_model
         return self._custom_quality_profile_drop_down_menu_model
 
 
+    def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
+        return self._cura_API
+
     ##  Registers objects for the QML engine to use.
     ##  Registers objects for the QML engine to use.
     #
     #
     #   \param engine The QML engine.
     #   \param engine The QML engine.
@@ -941,6 +955,9 @@ class CuraApplication(QtApplication):
         qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
         qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
         qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
         qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
 
 
+        from cura.API import CuraAPI
+        qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
+
         # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
         # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
         actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
         actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
         qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
         qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")

+ 4 - 4
cura/Machines/Models/SettingVisibilityPresetsModel.py

@@ -1,7 +1,7 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-from typing import Optional
+from typing import Optional, List, Dict, Union
 import os
 import os
 import urllib.parse
 import urllib.parse
 from configparser import ConfigParser
 from configparser import ConfigParser
@@ -60,7 +60,7 @@ class SettingVisibilityPresetsModel(ListModel):
 
 
     def _populate(self) -> None:
     def _populate(self) -> None:
         from cura.CuraApplication import CuraApplication
         from cura.CuraApplication import CuraApplication
-        items = []
+        items = [] # type: List[Dict[str, Union[str, int, List[str]]]]
         for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
         for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
             try:
             try:
                 mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
                 mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
@@ -79,7 +79,7 @@ class SettingVisibilityPresetsModel(ListModel):
                 if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
                 if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
                     continue
                     continue
 
 
-                settings = []
+                settings = []  # type: List[str]
                 for section in parser.sections():
                 for section in parser.sections():
                     if section == 'general':
                     if section == 'general':
                         continue
                         continue
@@ -98,7 +98,7 @@ class SettingVisibilityPresetsModel(ListModel):
             except Exception:
             except Exception:
                 Logger.logException("e", "Failed to load setting preset %s", file_path)
                 Logger.logException("e", "Failed to load setting preset %s", file_path)
 
 
-        items.sort(key = lambda k: (int(k["weight"]), k["id"]))
+        items.sort(key = lambda k: (int(k["weight"]), k["id"]))  # type: ignore
         # Put "custom" at the top
         # Put "custom" at the top
         items.insert(0, {"id": "custom",
         items.insert(0, {"id": "custom",
                          "name": "Custom selection",
                          "name": "Custom selection",

+ 4 - 6
cura/Machines/QualityManager.py

@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional, cast, Dict, List
 
 
 from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
 from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
 
 
-from UM.Application import Application
 from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
 from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
 from UM.Logger import Logger
 from UM.Logger import Logger
 from UM.Util import parseBool
 from UM.Util import parseBool
@@ -21,7 +20,6 @@ if TYPE_CHECKING:
     from cura.Settings.GlobalStack import GlobalStack
     from cura.Settings.GlobalStack import GlobalStack
     from .QualityChangesGroup import QualityChangesGroup
     from .QualityChangesGroup import QualityChangesGroup
     from cura.CuraApplication import CuraApplication
     from cura.CuraApplication import CuraApplication
-    from UM.Settings.ContainerRegistry import ContainerRegistry
 
 
 
 
 #
 #
@@ -38,11 +36,11 @@ class QualityManager(QObject):
 
 
     qualitiesUpdated = pyqtSignal()
     qualitiesUpdated = pyqtSignal()
 
 
-    def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
+    def __init__(self, application: "CuraApplication", parent = None) -> None:
         super().__init__(parent)
         super().__init__(parent)
-        self._application = Application.getInstance()  # type: CuraApplication
+        self._application = application
         self._material_manager = self._application.getMaterialManager()
         self._material_manager = self._application.getMaterialManager()
-        self._container_registry = container_registry
+        self._container_registry = self._application.getContainerRegistry()
 
 
         self._empty_quality_container = self._application.empty_quality_container
         self._empty_quality_container = self._application.empty_quality_container
         self._empty_quality_changes_container = self._application.empty_quality_changes_container
         self._empty_quality_changes_container = self._application.empty_quality_changes_container
@@ -458,7 +456,7 @@ class QualityManager(QObject):
     #   stack and clear the user settings.
     #   stack and clear the user settings.
     @pyqtSlot(str)
     @pyqtSlot(str)
     def createQualityChanges(self, base_name: str) -> None:
     def createQualityChanges(self, base_name: str) -> None:
-        machine_manager = Application.getInstance().getMachineManager()
+        machine_manager = self._application.getMachineManager()
 
 
         global_stack = machine_manager.activeMachine
         global_stack = machine_manager.activeMachine
         if not global_stack:
         if not global_stack:

Some files were not shown because too many files changed in this diff