Browse Source

Merge branch 'ui_rework_4_0' into cura4.0_header

Conflicts:
plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
resources/qml/Menus/ViewMenu.qml
resources/themes/cura-dark/theme.json
resources/themes/cura-light/theme.json
Diego Prado Gesto 6 years ago
parent
commit
5de367bcc4

+ 1 - 1
cura/API/Account.py

@@ -45,7 +45,7 @@ class Account(QObject):
             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",
+            CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.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)

+ 27 - 14
cura/CuraApplication.py

@@ -4,7 +4,7 @@
 import os
 import sys
 import time
-from typing import cast, TYPE_CHECKING, Optional
+from typing import cast, TYPE_CHECKING, Optional, Callable
 
 import numpy
 
@@ -13,6 +13,7 @@ from PyQt5.QtGui import QColor, QIcon
 from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
+from UM.Application import Application
 from UM.PluginError import PluginNotFoundError
 from UM.Scene.SceneNode import SceneNode
 from UM.Scene.Camera import Camera
@@ -114,12 +115,13 @@ from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
 from cura.ObjectsModel import ObjectsModel
 
 from UM.FlameProfiler import pyqtSlot
-
+from UM.Decorators import override
 
 if TYPE_CHECKING:
     from cura.Machines.MaterialManager import MaterialManager
     from cura.Machines.QualityManager import QualityManager
     from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
+    from cura.Settings.GlobalStack import GlobalStack
 
 
 numpy.seterr(all = "ignore")
@@ -166,6 +168,8 @@ class CuraApplication(QtApplication):
 
         self.default_theme = "cura-light"
 
+        self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
+
         self._boot_loading_time = time.time()
 
         self._on_exit_callback_manager = OnExitCallbackManager(self)
@@ -302,8 +306,6 @@ class CuraApplication(QtApplication):
         self._machine_action_manager = MachineActionManager.MachineActionManager(self)
         self._machine_action_manager.initialize()
 
-        self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
-
     def __sendCommandToSingleInstance(self):
         self._single_instance = SingleInstance(self, self._files_to_open)
 
@@ -419,7 +421,7 @@ class CuraApplication(QtApplication):
         )
 
     # Runs preparations that needs to be done before the starting process.
-    def startSplashWindowPhase(self):
+    def startSplashWindowPhase(self) -> None:
         super().startSplashWindowPhase()
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@@ -525,15 +527,15 @@ class CuraApplication(QtApplication):
         self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
 
     @pyqtProperty(bool)
-    def needToShowUserAgreement(self):
+    def needToShowUserAgreement(self) -> bool:
         return self._need_to_show_user_agreement
 
-    def setNeedToShowUserAgreement(self, set_value = True):
+    def setNeedToShowUserAgreement(self, set_value = True) -> None:
         self._need_to_show_user_agreement = set_value
 
     # DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
     # pre-exit checks such as checking for in-progress USB printing, etc.
-    def closeApplication(self):
+    def closeApplication(self) -> None:
         Logger.log("i", "Close application")
         main_window = self.getMainWindow()
         if main_window is not None:
@@ -560,11 +562,11 @@ class CuraApplication(QtApplication):
 
     showConfirmExitDialog = pyqtSignal(str, arguments = ["message"])
 
-    def setConfirmExitDialogCallback(self, callback):
+    def setConfirmExitDialogCallback(self, callback: Callable) -> None:
         self._confirm_exit_dialog_callback = callback
 
     @pyqtSlot(bool)
-    def callConfirmExitDialogCallback(self, yes_or_no: bool):
+    def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
         self._confirm_exit_dialog_callback(yes_or_no)
 
     ##  Signal to connect preferences action in QML
@@ -572,9 +574,17 @@ class CuraApplication(QtApplication):
 
     ##  Show the preferences window
     @pyqtSlot()
-    def showPreferences(self):
+    def showPreferences(self) -> None:
         self.showPreferencesWindow.emit()
 
+    @override(Application)
+    def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
+        return self._global_container_stack
+
+    @override(Application)
+    def setGlobalContainerStack(self, stack: "GlobalStack") -> None:
+        super().setGlobalContainerStack(stack)
+
     ## A reusable dialogbox
     #
     showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"])
@@ -586,7 +596,7 @@ class CuraApplication(QtApplication):
 
     showDiscardOrKeepProfileChanges = pyqtSignal()
 
-    def discardOrKeepProfileChanges(self):
+    def discardOrKeepProfileChanges(self) -> bool:
         has_user_interaction = False
         choice = self.getPreferences().getValue("cura/choice_on_profile_override")
         if choice == "always_discard":
@@ -602,7 +612,7 @@ class CuraApplication(QtApplication):
         return has_user_interaction
 
     @pyqtSlot(str)
-    def discardOrKeepProfileChangesClosed(self, option):
+    def discardOrKeepProfileChangesClosed(self, option: str) -> None:
         global_stack = self.getGlobalContainerStack()
         if option == "discard":
             for extruder in global_stack.extruders.values():
@@ -689,7 +699,7 @@ class CuraApplication(QtApplication):
         self._quality_manager.initialize()
 
         Logger.log("i", "Initializing machine manager")
-        self._machine_manager = MachineManager(self)
+        self._machine_manager = MachineManager(self, parent = self)
 
         Logger.log("i", "Initializing container manager")
         self._container_manager = ContainerManager(self)
@@ -947,6 +957,9 @@ class CuraApplication(QtApplication):
         qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
         qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
 
+        from cura.PrinterOutput.CameraView import CameraView
+        qmlRegisterType(CameraView, "Cura", 1, 0, "CameraView")
+
         qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
                                  "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
         qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,

+ 20 - 14
cura/MachineAction.py

@@ -1,13 +1,13 @@
 # Copyright (c) 2016 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+import os
+
 from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
 
+from UM.Logger import Logger
 from UM.PluginObject import PluginObject
 from UM.PluginRegistry import PluginRegistry
-from UM.Application import Application
-
-import os
 
 
 ##  Machine actions are actions that are added to a specific machine type. Examples of such actions are
@@ -19,7 +19,7 @@ class MachineAction(QObject, PluginObject):
     ##  Create a new Machine action.
     #   \param key unique key of the machine action
     #   \param label Human readable label used to identify the machine action.
-    def __init__(self, key, label = ""):
+    def __init__(self, key: str, label: str = "") -> None:
         super().__init__()
         self._key = key
         self._label = label
@@ -30,14 +30,14 @@ class MachineAction(QObject, PluginObject):
     labelChanged = pyqtSignal()
     onFinished = pyqtSignal()
 
-    def getKey(self):
+    def getKey(self) -> str:
         return self._key
 
     @pyqtProperty(str, notify = labelChanged)
-    def label(self):
+    def label(self) -> str:
         return self._label
 
-    def setLabel(self, label):
+    def setLabel(self, label: str) -> None:
         if self._label != label:
             self._label = label
             self.labelChanged.emit()
@@ -46,29 +46,35 @@ class MachineAction(QObject, PluginObject):
     #   This should not be re-implemented by child classes, instead re-implement _reset.
     #   /sa _reset
     @pyqtSlot()
-    def reset(self):
+    def reset(self) -> None:
         self._finished = False
         self._reset()
 
     ##  Protected implementation of reset.
     #   /sa reset()
-    def _reset(self):
+    def _reset(self) -> None:
         pass
 
     @pyqtSlot()
-    def setFinished(self):
+    def setFinished(self) -> None:
         self._finished = True
         self._reset()
         self.onFinished.emit()
 
     @pyqtProperty(bool, notify = onFinished)
-    def finished(self):
+    def finished(self) -> bool:
         return self._finished
 
     ##  Protected helper to create a view object based on provided QML.
-    def _createViewFromQML(self):
-        path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)
-        self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
+    def _createViewFromQML(self) -> None:
+        plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
+        if plugin_path is None:
+            Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
+            return
+        path = os.path.join(plugin_path, self._qml_url)
+
+        from cura.CuraApplication import CuraApplication
+        self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
 
     @pyqtProperty(QObject, constant = True)
     def displayItem(self):

+ 51 - 40
cura/MachineActionManager.py

@@ -1,12 +1,18 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from typing import TYPE_CHECKING, Optional, List, Set, Dict
+
 from PyQt5.QtCore import QObject
 
 from UM.FlameProfiler import pyqtSlot
 from UM.Logger import Logger
 from UM.PluginRegistry import PluginRegistry  # So MachineAction can be added as plugin type
-from UM.Settings.DefinitionContainer import DefinitionContainer
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+    from cura.Settings.GlobalStack import GlobalStack
+    from .MachineAction import MachineAction
 
 
 ##  Raised when trying to add an unknown machine action as a required action
@@ -20,46 +26,54 @@ class NotUniqueMachineActionError(Exception):
 
 
 class MachineActionManager(QObject):
-    def __init__(self, application, parent = None):
-        super().__init__(parent)
+    def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
+        super().__init__(parent = parent)
         self._application = application
+        self._container_registry = self._application.getContainerRegistry()
 
-        self._machine_actions = {}  # Dict of all known machine actions
-        self._required_actions = {}  # Dict of all required actions by definition ID
-        self._supported_actions = {}  # Dict of all supported actions by definition ID
-        self._first_start_actions = {}  # Dict of all actions that need to be done when first added by definition ID
+        # Keeps track of which machines have already been processed so we don't do that again.
+        self._definition_ids_with_default_actions_added = set()  # type: Set[str]
 
-    def initialize(self):
-        container_registry = self._application.getContainerRegistry()
+        # Dict of all known machine actions
+        self._machine_actions = {}  # type: Dict[str, MachineAction]
+        # Dict of all required actions by definition ID
+        self._required_actions = {}  # type: Dict[str, List[MachineAction]]
+        # Dict of all supported actions by definition ID
+        self._supported_actions = {}  # type: Dict[str, List[MachineAction]]
+        # Dict of all actions that need to be done when first added by definition ID
+        self._first_start_actions = {}  # type: Dict[str, List[MachineAction]]
 
+    def initialize(self):
         # Add machine_action as plugin type
         PluginRegistry.addType("machine_action", self.addMachineAction)
 
-        # Ensure that all containers that were registered before creation of this registry are also handled.
-        # This should not have any effect, but it makes it safer if we ever refactor the order of things.
-        for container in container_registry.findDefinitionContainers():
-            self._onContainerAdded(container)
+    # Adds all default machine actions that are defined in the machine definition for the given machine.
+    def addDefaultMachineActions(self, global_stack: "GlobalStack") -> None:
+        definition_id = global_stack.definition.getId()
+
+        if definition_id in self._definition_ids_with_default_actions_added:
+            Logger.log("i", "Default machine actions have been added for machine definition [%s], do nothing.",
+                       definition_id)
+            return
 
-        container_registry.containerAdded.connect(self._onContainerAdded)
+        supported_actions = global_stack.getMetaDataEntry("supported_actions", [])
+        for action_key in supported_actions:
+            self.addSupportedAction(definition_id, action_key)
 
-    def _onContainerAdded(self, container):
-        ## Ensure that the actions are added to this manager
-        if isinstance(container, DefinitionContainer):
-            supported_actions = container.getMetaDataEntry("supported_actions", [])
-            for action in supported_actions:
-                self.addSupportedAction(container.getId(), action)
+        required_actions = global_stack.getMetaDataEntry("required_actions", [])
+        for action_key in required_actions:
+            self.addRequiredAction(definition_id, action_key)
 
-            required_actions = container.getMetaDataEntry("required_actions", [])
-            for action in required_actions:
-                self.addRequiredAction(container.getId(), action)
+        first_start_actions = global_stack.getMetaDataEntry("first_start_actions", [])
+        for action_key in first_start_actions:
+            self.addFirstStartAction(definition_id, action_key)
 
-            first_start_actions = container.getMetaDataEntry("first_start_actions", [])
-            for action in first_start_actions:
-                self.addFirstStartAction(container.getId(), action)
+        self._definition_ids_with_default_actions_added.add(definition_id)
+        Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id)
 
     ##  Add a required action to a machine
     #   Raises an exception when the action is not recognised.
-    def addRequiredAction(self, definition_id, action_key):
+    def addRequiredAction(self, definition_id: str, action_key: str) -> None:
         if action_key in self._machine_actions:
             if definition_id in self._required_actions:
                 if self._machine_actions[action_key] not in self._required_actions[definition_id]:
@@ -70,7 +84,7 @@ class MachineActionManager(QObject):
             raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
 
     ##  Add a supported action to a machine.
-    def addSupportedAction(self, definition_id, action_key):
+    def addSupportedAction(self, definition_id: str, action_key: str) -> None:
         if action_key in self._machine_actions:
             if definition_id in self._supported_actions:
                 if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
@@ -81,13 +95,10 @@ class MachineActionManager(QObject):
             Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
 
     ##  Add an action to the first start list of a machine.
-    def addFirstStartAction(self, definition_id, action_key, index = None):
+    def addFirstStartAction(self, definition_id: str, action_key: str) -> None:
         if action_key in self._machine_actions:
             if definition_id in self._first_start_actions:
-                if index is not None:
-                    self._first_start_actions[definition_id].insert(index, self._machine_actions[action_key])
-                else:
-                    self._first_start_actions[definition_id].append(self._machine_actions[action_key])
+                self._first_start_actions[definition_id].append(self._machine_actions[action_key])
             else:
                 self._first_start_actions[definition_id] = [self._machine_actions[action_key]]
         else:
@@ -95,7 +106,7 @@ class MachineActionManager(QObject):
 
     ##  Add a (unique) MachineAction
     #   if the Key of the action is not unique, an exception is raised.
-    def addMachineAction(self, action):
+    def addMachineAction(self, action: "MachineAction") -> None:
         if action.getKey() not in self._machine_actions:
             self._machine_actions[action.getKey()] = action
         else:
@@ -105,7 +116,7 @@ class MachineActionManager(QObject):
     #   \param definition_id The ID of the definition you want the supported actions of
     #   \returns set of supported actions.
     @pyqtSlot(str, result = "QVariantList")
-    def getSupportedActions(self, definition_id):
+    def getSupportedActions(self, definition_id: str) -> List["MachineAction"]:
         if definition_id in self._supported_actions:
             return list(self._supported_actions[definition_id])
         else:
@@ -114,11 +125,11 @@ class MachineActionManager(QObject):
     ##  Get all actions required by given machine
     #   \param definition_id The ID of the definition you want the required actions of
     #   \returns set of required actions.
-    def getRequiredActions(self, definition_id):
+    def getRequiredActions(self, definition_id: str) -> List["MachineAction"]:
         if definition_id in self._required_actions:
             return self._required_actions[definition_id]
         else:
-            return set()
+            return list()
 
     ##  Get all actions that need to be performed upon first start of a given machine.
     #   Note that contrary to required / supported actions a list is returned (as it could be required to run the same
@@ -126,7 +137,7 @@ class MachineActionManager(QObject):
     #   \param definition_id The ID of the definition that you want to get the "on added" actions for.
     #   \returns List of actions.
     @pyqtSlot(str, result="QVariantList")
-    def getFirstStartActions(self, definition_id):
+    def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
         if definition_id in self._first_start_actions:
             return self._first_start_actions[definition_id]
         else:
@@ -134,7 +145,7 @@ class MachineActionManager(QObject):
 
     ##  Remove Machine action from manager
     #   \param action to remove
-    def removeMachineAction(self, action):
+    def removeMachineAction(self, action: "MachineAction") -> None:
         try:
             del self._machine_actions[action.getKey()]
         except KeyError:
@@ -143,7 +154,7 @@ class MachineActionManager(QObject):
     ##  Get MachineAction by key
     #   \param key String of key to select
     #   \return Machine action if found, None otherwise
-    def getMachineAction(self, key):
+    def getMachineAction(self, key: str) -> Optional["MachineAction"]:
         if key in self._machine_actions:
             return self._machine_actions[key]
         else:

+ 15 - 5
cura/Machines/MaterialManager.py

@@ -365,7 +365,7 @@ class MaterialManager(QObject):
         nozzle_name = None
         if extruder_stack.variant.getId() != "empty_variant":
             nozzle_name = extruder_stack.variant.getName()
-        diameter = extruder_stack.approximateMaterialDiameter
+        diameter = extruder_stack.getApproximateMaterialDiameter()
 
         # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
         return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
@@ -478,12 +478,22 @@ class MaterialManager(QObject):
 
         buildplate_name = global_stack.getBuildplateName()
         machine_definition = global_stack.definition
+
+        # The extruder-compatible material diameter in the extruder definition may not be the correct value because
+        # the user can change it in the definition_changes container.
         if extruder_definition is None:
-            extruder_definition = global_stack.extruders[position].definition
+            extruder_stack_or_definition = global_stack.extruders[position]
+            is_extruder_stack = True
+        else:
+            extruder_stack_or_definition = extruder_definition
+            is_extruder_stack = False
+
+        if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
+            if is_extruder_stack:
+                material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
+            else:
+                material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")
 
-        if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
-            # At this point the extruder_definition is not None
-            material_diameter = extruder_definition.getProperty("material_diameter", "value")
             if isinstance(material_diameter, SettingFunction):
                 material_diameter = material_diameter(global_stack)
             approximate_material_diameter = str(round(material_diameter))

+ 2 - 0
cura/Machines/Models/BaseMaterialsModel.py

@@ -64,9 +64,11 @@ class BaseMaterialsModel(ListModel):
 
         if self._extruder_stack is not None:
             self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
+            self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update)
         self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
         if self._extruder_stack is not None:
             self._extruder_stack.pyqtContainersChanged.connect(self._update)
+            self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update)
         # Force update the model when the extruder stack changes
         self._update()
 

+ 101 - 107
cura/PrintInformation.py

@@ -6,7 +6,7 @@ import math
 import os
 import unicodedata
 import re  # To create abbreviations for printer names.
-from typing import Dict
+from typing import Dict, List, Optional
 
 from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
 
@@ -16,55 +16,41 @@ from UM.Scene.SceneNode import SceneNode
 from UM.i18n import i18nCatalog
 from UM.MimeTypeDatabase import MimeTypeDatabase
 
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
 catalog = i18nCatalog("cura")
 
 
-##  A class for processing and calculating minimum, current and maximum print time as well as managing the job name
-#
-#   This class contains all the logic relating to calculation and slicing for the
-#   time/quality slider concept. It is a rather tricky combination of event handling
-#   and state management. The logic behind this is as follows:
-#
-#   - A scene change or setting change event happens.
-#        We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
-#   - This triggers a new slice with the current settings - this is the "current settings pass".
-#   - When the slice is done, we update the current print time and material amount.
-#   - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
-#   - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
-#   - When the Extra Fine pass is done, we update the maximum print time.
+##  A class for processing and the print times per build plate as well as managing the job name
 #
 #   This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
 #   This job name is requested by the JobSpecs qml file.
 class PrintInformation(QObject):
-    class SlicePass:
-        CurrentSettings = 1
-        LowQualitySettings = 2
-        HighQualitySettings = 3
-
-    class SliceReason:
-        SceneChanged = 1
-        SettingChanged = 2
-        ActiveMachineChanged = 3
-        Other = 4
-
-    def __init__(self, application, parent = None):
+
+    UNTITLED_JOB_NAME = "Untitled"
+
+    def __init__(self, application: "CuraApplication", parent = None) -> None:
         super().__init__(parent)
         self._application = application
 
-        self.UNTITLED_JOB_NAME = "Untitled"
-
         self.initializeCuraMessagePrintTimeProperties()
 
-        self._material_lengths = {}  # indexed by build plate number
-        self._material_weights = {}
-        self._material_costs = {}
-        self._material_names = {}
+        # Indexed by build plate number
+        self._material_lengths = {}  # type: Dict[int, List[float]]
+        self._material_weights = {}  # type: Dict[int, List[float]]
+        self._material_costs = {}   # type: Dict[int, List[float]]
+        self._material_names = {}  # type: Dict[int, List[str]]
 
         self._pre_sliced = False
 
         self._backend = self._application.getBackend()
         if self._backend:
             self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
+
         self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
 
         self._is_user_specified_job_name = False
@@ -72,29 +58,25 @@ class PrintInformation(QObject):
         self._abbr_machine = ""
         self._job_name = ""
         self._active_build_plate = 0
-        self._initVariablesWithBuildPlate(self._active_build_plate)
+        self._initVariablesByBuildPlate(self._active_build_plate)
 
         self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
 
-        ss = self._multi_build_plate_model.maxBuildPlate
-
         self._application.globalContainerStackChanged.connect(self._updateJobName)
         self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
         self._application.fileLoaded.connect(self.setBaseName)
         self._application.workspaceLoaded.connect(self.setProjectName)
-        self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
-
+        self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
         self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
 
-        self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
+        self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
+
         self._onActiveMaterialsChanged()
 
-        self._material_amounts = []
+        self._material_amounts = []  # type: List[float]
 
-    # Crate cura message translations and using translation keys initialize empty time Duration object for total time
-    # and time for each feature
-    def initializeCuraMessagePrintTimeProperties(self):
-        self._current_print_time = {}  # Duration(None, self)
+    def initializeCuraMessagePrintTimeProperties(self) -> None:
+        self._current_print_time = {}  # type: Dict[int, Duration]
 
         self._print_time_message_translations = {
             "inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
@@ -110,17 +92,17 @@ class PrintInformation(QObject):
             "none": catalog.i18nc("@tooltip", "Other")
         }
 
-        self._print_time_message_values = {}
+        self._print_times_per_feature = {}  # type: Dict[int, Dict[str, Duration]]
 
-    def _initPrintTimeMessageValues(self, build_plate_number):
+    def _initPrintTimesPerFeature(self, build_plate_number: int) -> None:
         # Full fill message values using keys from _print_time_message_translations
-        self._print_time_message_values[build_plate_number] = {}
+        self._print_times_per_feature[build_plate_number] = {}
         for key in self._print_time_message_translations.keys():
-            self._print_time_message_values[build_plate_number][key] = Duration(None, self)
+            self._print_times_per_feature[build_plate_number][key] = Duration(None, self)
 
-    def _initVariablesWithBuildPlate(self, build_plate_number):
-        if build_plate_number not in self._print_time_message_values:
-            self._initPrintTimeMessageValues(build_plate_number)
+    def _initVariablesByBuildPlate(self, build_plate_number: int) -> None:
+        if build_plate_number not in self._print_times_per_feature:
+            self._initPrintTimesPerFeature(build_plate_number)
         if self._active_build_plate not in self._material_lengths:
             self._material_lengths[self._active_build_plate] = []
         if self._active_build_plate not in self._material_weights:
@@ -130,23 +112,24 @@ class PrintInformation(QObject):
         if self._active_build_plate not in self._material_names:
             self._material_names[self._active_build_plate] = []
         if self._active_build_plate not in self._current_print_time:
-            self._current_print_time[self._active_build_plate] = Duration(None, self)
+            self._current_print_time[self._active_build_plate] = Duration(parent = self)
 
     currentPrintTimeChanged = pyqtSignal()
 
     preSlicedChanged = pyqtSignal()
 
     @pyqtProperty(bool, notify=preSlicedChanged)
-    def preSliced(self):
+    def preSliced(self) -> bool:
         return self._pre_sliced
 
-    def setPreSliced(self, pre_sliced):
-        self._pre_sliced = pre_sliced
-        self._updateJobName()
-        self.preSlicedChanged.emit()
+    def setPreSliced(self, pre_sliced: bool) -> None:
+        if self._pre_sliced != pre_sliced:
+            self._pre_sliced = pre_sliced
+            self._updateJobName()
+            self.preSlicedChanged.emit()
 
     @pyqtProperty(Duration, notify = currentPrintTimeChanged)
-    def currentPrintTime(self):
+    def currentPrintTime(self) -> Duration:
         return self._current_print_time[self._active_build_plate]
 
     materialLengthsChanged = pyqtSignal()
@@ -173,36 +156,41 @@ class PrintInformation(QObject):
     def materialNames(self):
         return self._material_names[self._active_build_plate]
 
-    def printTimes(self):
-        return self._print_time_message_values[self._active_build_plate]
+    #   Get all print times (by feature) of the active buildplate.
+    def printTimes(self) -> Dict[str, Duration]:
+        return self._print_times_per_feature[self._active_build_plate]
 
-    def _onPrintDurationMessage(self, build_plate_number, print_time: Dict[str, int], material_amounts: list):
-        self._updateTotalPrintTimePerFeature(build_plate_number, print_time)
+    def _onPrintDurationMessage(self, build_plate_number: int, print_times_per_feature: Dict[str, int], material_amounts: List[float]) -> None:
+        self._updateTotalPrintTimePerFeature(build_plate_number, print_times_per_feature)
         self.currentPrintTimeChanged.emit()
 
         self._material_amounts = material_amounts
         self._calculateInformation(build_plate_number)
 
-    def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
+    def _updateTotalPrintTimePerFeature(self, build_plate_number: int, print_times_per_feature: Dict[str, int]) -> None:
         total_estimated_time = 0
 
-        if build_plate_number not in self._print_time_message_values:
-            self._initPrintTimeMessageValues(build_plate_number)
+        if build_plate_number not in self._print_times_per_feature:
+            self._initPrintTimesPerFeature(build_plate_number)
+
+        for feature, time in print_times_per_feature.items():
+            if feature not in self._print_times_per_feature[build_plate_number]:
+                self._print_times_per_feature[build_plate_number][feature] = Duration(parent=self)
+            duration = self._print_times_per_feature[build_plate_number][feature]
 
-        for feature, time in print_time.items():
             if time != time:  # Check for NaN. Engine can sometimes give us weird values.
-                self._print_time_message_values[build_plate_number].get(feature).setDuration(0)
+                duration.setDuration(0)
                 Logger.log("w", "Received NaN for print duration message")
                 continue
 
             total_estimated_time += time
-            self._print_time_message_values[build_plate_number].get(feature).setDuration(time)
+            duration.setDuration(time)
 
         if build_plate_number not in self._current_print_time:
             self._current_print_time[build_plate_number] = Duration(None, self)
         self._current_print_time[build_plate_number].setDuration(total_estimated_time)
 
-    def _calculateInformation(self, build_plate_number):
+    def _calculateInformation(self, build_plate_number: int) -> None:
         global_stack = self._application.getGlobalContainerStack()
         if global_stack is None:
             return
@@ -215,39 +203,45 @@ class PrintInformation(QObject):
         material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings"))
 
         extruder_stacks = global_stack.extruders
-        for position, extruder_stack in extruder_stacks.items():
+
+        for position in extruder_stacks:
+            extruder_stack = extruder_stacks[position]
             index = int(position)
             if index >= len(self._material_amounts):
                 continue
             amount = self._material_amounts[index]
-            ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
-            #  list comprehension filtering to solve this for us.
+            # Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
+            # list comprehension filtering to solve this for us.
             density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
-            material = extruder_stack.findContainer({"type": "material"})
+            material = extruder_stack.material
             radius = extruder_stack.getProperty("material_diameter", "value") / 2
 
             weight = float(amount) * float(density) / 1000
-            cost = 0
-            material_name = catalog.i18nc("@label unknown material", "Unknown")
-            if material:
-                material_guid = material.getMetaDataEntry("GUID")
-                material_name = material.getName()
-                if material_guid in material_preference_values:
-                    material_values = material_preference_values[material_guid]
-
-                    weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
-                    cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
-
-                    if weight_per_spool != 0:
-                        cost = cost_per_spool * weight / weight_per_spool
-                    else:
-                        cost = 0
+            cost = 0.
+
+            material_guid = material.getMetaDataEntry("GUID")
+            material_name = material.getName()
+            if material_guid in material_preference_values:
+                material_values = material_preference_values[material_guid]
+
+                if material_values and "spool_weight" in material_values:
+                    weight_per_spool = float(material_values["spool_weight"])
+                else:
+                    weight_per_spool = float(extruder_stack.getMetaDataEntry("properties", {}).get("weight", 0))
+
+                cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
+
+                if weight_per_spool != 0:
+                    cost = cost_per_spool * weight / weight_per_spool
+                else:
+                    cost = 0
 
             # Material amount is sent as an amount of mm^3, so calculate length from that
             if radius != 0:
                 length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
             else:
                 length = 0
+
             self._material_weights[build_plate_number].append(weight)
             self._material_lengths[build_plate_number].append(length)
             self._material_costs[build_plate_number].append(cost)
@@ -258,20 +252,20 @@ class PrintInformation(QObject):
         self.materialCostsChanged.emit()
         self.materialNamesChanged.emit()
 
-    def _onPreferencesChanged(self, preference):
+    def _onPreferencesChanged(self, preference: str) -> None:
         if preference != "cura/material_settings":
             return
 
         for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
             self._calculateInformation(build_plate_number)
 
-    def _onActiveBuildPlateChanged(self):
+    def _onActiveBuildPlateChanged(self) -> None:
         new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
         if new_active_build_plate != self._active_build_plate:
             self._active_build_plate = new_active_build_plate
             self._updateJobName()
 
-            self._initVariablesWithBuildPlate(self._active_build_plate)
+            self._initVariablesByBuildPlate(self._active_build_plate)
 
             self.materialLengthsChanged.emit()
             self.materialWeightsChanged.emit()
@@ -279,14 +273,14 @@ class PrintInformation(QObject):
             self.materialNamesChanged.emit()
             self.currentPrintTimeChanged.emit()
 
-    def _onActiveMaterialsChanged(self, *args, **kwargs):
+    def _onActiveMaterialsChanged(self, *args, **kwargs) -> None:
         for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
             self._calculateInformation(build_plate_number)
 
     # Manual override of job name should also set the base name so that when the printer prefix is updated, it the
     # prefix can be added to the manually added name, not the old base name
     @pyqtSlot(str, bool)
-    def setJobName(self, name, is_user_specified_job_name = False):
+    def setJobName(self, name: str, is_user_specified_job_name = False) -> None:
         self._is_user_specified_job_name = is_user_specified_job_name
         self._job_name = name
         self._base_name = name.replace(self._abbr_machine + "_", "")
@@ -300,7 +294,7 @@ class PrintInformation(QObject):
     def jobName(self):
         return self._job_name
 
-    def _updateJobName(self):
+    def _updateJobName(self) -> None:
         if self._base_name == "":
             self._job_name = self.UNTITLED_JOB_NAME
             self._is_user_specified_job_name = False
@@ -335,12 +329,12 @@ class PrintInformation(QObject):
         self.jobNameChanged.emit()
 
     @pyqtSlot(str)
-    def setProjectName(self, name):
+    def setProjectName(self, name: str) -> None:
         self.setBaseName(name, is_project_file = True)
 
     baseNameChanged = pyqtSignal()
 
-    def setBaseName(self, base_name: str, is_project_file: bool = False):
+    def setBaseName(self, base_name: str, is_project_file: bool = False) -> None:
         self._is_user_specified_job_name = False
 
         # Ensure that we don't use entire path but only filename
@@ -384,7 +378,7 @@ class PrintInformation(QObject):
     ##  Created an acronym-like abbreviated machine name from the currently
     #   active machine name.
     #   Called each time the global stack is switched.
-    def _defineAbbreviatedMachineName(self):
+    def _defineAbbreviatedMachineName(self) -> None:
         global_container_stack = self._application.getGlobalContainerStack()
         if not global_container_stack:
             self._abbr_machine = ""
@@ -408,15 +402,15 @@ class PrintInformation(QObject):
         self._abbr_machine = abbr_machine
 
     ##  Utility method that strips accents from characters (eg: â -> a)
-    def _stripAccents(self, str):
-        return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
+    def _stripAccents(self, to_strip: str) -> str:
+        return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
 
     @pyqtSlot(result = "QVariantMap")
     def getFeaturePrintTimes(self):
         result = {}
-        if self._active_build_plate not in self._print_time_message_values:
-            self._initPrintTimeMessageValues(self._active_build_plate)
-        for feature, time in self._print_time_message_values[self._active_build_plate].items():
+        if self._active_build_plate not in self._print_times_per_feature:
+            self._initPrintTimesPerFeature(self._active_build_plate)
+        for feature, time in self._print_times_per_feature[self._active_build_plate].items():
             if feature in self._print_time_message_translations:
                 result[self._print_time_message_translations[feature]] = time
             else:
@@ -424,22 +418,22 @@ class PrintInformation(QObject):
         return result
 
     # Simulate message with zero time duration
-    def setToZeroPrintInformation(self, build_plate = None):
+    def setToZeroPrintInformation(self, build_plate: Optional[int] = None) -> None:
         if build_plate is None:
             build_plate = self._active_build_plate
 
         # Construct the 0-time message
         temp_message = {}
-        if build_plate not in self._print_time_message_values:
-            self._print_time_message_values[build_plate] = {}
-        for key in self._print_time_message_values[build_plate].keys():
+        if build_plate not in self._print_times_per_feature:
+            self._print_times_per_feature[build_plate] = {}
+        for key in self._print_times_per_feature[build_plate].keys():
             temp_message[key] = 0
-        temp_material_amounts = [0]
+        temp_material_amounts = [0.]
 
         self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts)
 
     ##  Listen to scene changes to check if we need to reset the print information
-    def _onSceneChanged(self, scene_node):
+    def _onSceneChanged(self, scene_node: SceneNode) -> None:
         # Ignore any changes that are not related to sliceable objects
         if not isinstance(scene_node, SceneNode)\
                 or not scene_node.callDecoration("isSliceable")\

+ 41 - 0
cura/PrinterOutput/CameraView.py

@@ -0,0 +1,41 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtCore import pyqtProperty, pyqtSignal
+from PyQt5.QtGui import QImage
+from PyQt5.QtQuick import QQuickPaintedItem
+
+
+#
+# A custom camera view that uses QQuickPaintedItem to present (or "paint") the image frames from a printer's
+# network camera feed.
+#
+class CameraView(QQuickPaintedItem):
+
+    def __init__(self, *args, **kwargs) -> None:
+        super().__init__(*args, **kwargs)
+
+        self._image = QImage()
+
+    imageChanged = pyqtSignal()
+
+    def setImage(self, image: "QImage") -> None:
+        self._image = image
+        self.imageChanged.emit()
+        self.update()
+
+    def getImage(self) -> "QImage":
+        return self._image
+
+    image = pyqtProperty(QImage, fget = getImage, fset = setImage, notify = imageChanged)
+
+    @pyqtProperty(int, notify = imageChanged)
+    def imageWidth(self) -> int:
+        return self._image.width()
+
+    @pyqtProperty(int, notify = imageChanged)
+    def imageHeight(self) -> int:
+        return self._image.height()
+
+    def paint(self, painter):
+        painter.drawImage(self.contentsBoundingRect(), self._image)

+ 3 - 10
cura/PrinterOutput/NetworkCamera.py

@@ -16,7 +16,6 @@ class NetworkCamera(QObject):
         self._image_request = None
         self._image_reply = None
         self._image = QImage()
-        self._image_id = 0
 
         self._target = target
         self._started = False
@@ -33,15 +32,9 @@ class NetworkCamera(QObject):
         if restart_required:
             self.start()
 
-    @pyqtProperty(QUrl, notify=newImage)
+    @pyqtProperty(QImage, notify=newImage)
     def latestImage(self):
-        self._image_id += 1
-        # There is an image provider that is called "camera". In order to ensure that the image qml object, that
-        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
-        # as new (instead of relying on cached version and thus forces an update.
-        temp = "image://camera/" + str(self._image_id)
-
-        return QUrl(temp, QUrl.TolerantMode)
+        return self._image
 
     @pyqtSlot()
     def start(self):
@@ -116,4 +109,4 @@ class NetworkCamera(QObject):
             self._stream_buffer_start_index = -1
             self._image.loadFromData(jpg_data)
 
-        self.newImage.emit()
+            self.newImage.emit()

+ 2 - 3
cura/PrinterOutput/PrintJobOutputModel.py

@@ -1,4 +1,4 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
@@ -12,7 +12,6 @@ if TYPE_CHECKING:
     from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
     from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
 
-
 class PrintJobOutputModel(QObject):
     stateChanged = pyqtSignal()
     timeTotalChanged = pyqtSignal()
@@ -147,4 +146,4 @@ class PrintJobOutputModel(QObject):
 
     @pyqtSlot(str)
     def setState(self, state):
-        self._output_controller.setJobState(self, state)
+        self._output_controller.setJobState(self, state)

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