Browse Source

Merge pull request #3485 from Ultimaker/CURA-5059_reset_icon_visibility

Cura 5059 reset icon visibility
Lipu Fei 7 years ago
parent
commit
e91b955f1e

+ 19 - 6
cura/CuraApplication.py

@@ -67,6 +67,8 @@ from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
 from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
 from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel
 
+from cura.Machines.MachineErrorChecker import MachineErrorChecker
+
 from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
 from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
 
@@ -142,12 +144,6 @@ class CuraApplication(QtApplication):
 
     Q_ENUMS(ResourceTypes)
 
-    # FIXME: This signal belongs to the MachineManager, but the CuraEngineBackend plugin requires on it.
-    #        Because plugins are initialized before the ContainerRegistry, putting this signal in MachineManager
-    #        will make it initialized before ContainerRegistry does, and it won't find the active machine, thus
-    #        Cura will always show the Add Machine Dialog upon start.
-    stacksValidationFinished = pyqtSignal()  # Emitted whenever a validation is finished
-
     def __init__(self, **kwargs):
         # this list of dir names will be used by UM to detect an old cura directory
         for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
@@ -224,12 +220,14 @@ class CuraApplication(QtApplication):
         self._machine_manager = None    # This is initialized on demand.
         self._extruder_manager = None
         self._material_manager = None
+        self._quality_manager = None
         self._object_manager = None
         self._build_plate_model = None
         self._multi_build_plate_model = None
         self._setting_inheritance_manager = None
         self._simple_mode_settings_manager = None
         self._cura_scene_controller = None
+        self._machine_error_checker = None
 
         self._additional_components = {} # Components to add to certain areas in the interface
 
@@ -743,19 +741,28 @@ class CuraApplication(QtApplication):
         self.preRun()
 
         container_registry = ContainerRegistry.getInstance()
+
+        Logger.log("i", "Initializing variant manager")
         self._variant_manager = VariantManager(container_registry)
         self._variant_manager.initialize()
 
+        Logger.log("i", "Initializing material manager")
         from cura.Machines.MaterialManager import MaterialManager
         self._material_manager = MaterialManager(container_registry, parent = self)
         self._material_manager.initialize()
 
+        Logger.log("i", "Initializing quality manager")
         from cura.Machines.QualityManager import QualityManager
         self._quality_manager = QualityManager(container_registry, parent = self)
         self._quality_manager.initialize()
 
+        Logger.log("i", "Initializing machine manager")
         self._machine_manager = MachineManager(self)
 
+        Logger.log("i", "Initializing machine error checker")
+        self._machine_error_checker = MachineErrorChecker(self)
+        self._machine_error_checker.initialize()
+
         # Check if we should run as single instance or not
         self._setUpSingleInstanceServer()
 
@@ -781,8 +788,11 @@ class CuraApplication(QtApplication):
             self._openFile(file_name)
 
         self.started = True
+        self.initializationFinished.emit()
         self.exec_()
 
+    initializationFinished = pyqtSignal()
+
     ##  Run Cura without GUI elements and interaction (server mode).
     def runWithoutGUI(self):
         self._use_gui = False
@@ -847,6 +857,9 @@ class CuraApplication(QtApplication):
     def hasGui(self):
         return self._use_gui
 
+    def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
+        return self._machine_error_checker
+
     def getMachineManager(self, *args) -> MachineManager:
         if self._machine_manager is None:
             self._machine_manager = MachineManager(self)

+ 181 - 0
cura/Machines/MachineErrorChecker.py

@@ -0,0 +1,181 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import time
+
+from collections import deque
+
+from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
+
+from UM.Application import Application
+from UM.Logger import Logger
+from UM.Settings.SettingDefinition import SettingDefinition
+from UM.Settings.Validator import ValidatorState
+
+
+#
+# This class performs setting error checks for the currently active machine.
+#
+# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag.
+# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key
+# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should
+# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
+# for it to finish the complete work.
+#
+class MachineErrorChecker(QObject):
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self._global_stack = None
+
+        self._has_errors = True  # Result of the error check, indicating whether there are errors in the stack
+        self._error_keys = set()  # A set of settings keys that have errors
+        self._error_keys_in_progress = set()  # The variable that stores the results of the currently in progress check
+
+        self._stacks_and_keys_to_check = None  # a FIFO queue of tuples (stack, key) to check for errors
+
+        self._need_to_check = False  # Whether we need to schedule a new check or not. This flag is set when a new
+                                     # error check needs to take place while there is already one running at the moment.
+        self._check_in_progress = False  # Whether there is an error check running in progress at the moment.
+
+        self._application = Application.getInstance()
+        self._machine_manager = self._application.getMachineManager()
+
+        self._start_time = 0  # measure checking time
+
+        # This timer delays the starting of error check so we can react less frequently if the user is frequently
+        # changing settings.
+        self._error_check_timer = QTimer(self)
+        self._error_check_timer.setInterval(100)
+        self._error_check_timer.setSingleShot(True)
+
+    def initialize(self):
+        self._error_check_timer.timeout.connect(self._rescheduleCheck)
+
+        # Reconnect all signals when the active machine gets changed.
+        self._machine_manager.globalContainerChanged.connect(self._onMachineChanged)
+
+        # Whenever the machine settings get changed, we schedule an error check.
+        self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
+        self._machine_manager.globalValueChanged.connect(self.startErrorCheck)
+
+        self._onMachineChanged()
+
+    def _onMachineChanged(self):
+        if self._global_stack:
+            self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
+            self._global_stack.containersChanged.disconnect(self.startErrorCheck)
+
+            for extruder in self._global_stack.extruders.values():
+                extruder.propertyChanged.disconnect(self.startErrorCheck)
+                extruder.containersChanged.disconnect(self.startErrorCheck)
+
+        self._global_stack = self._machine_manager.activeMachine
+
+        if self._global_stack:
+            self._global_stack.propertyChanged.connect(self.startErrorCheck)
+            self._global_stack.containersChanged.connect(self.startErrorCheck)
+
+            for extruder in self._global_stack.extruders.values():
+                extruder.propertyChanged.connect(self.startErrorCheck)
+                extruder.containersChanged.connect(self.startErrorCheck)
+
+    hasErrorUpdated = pyqtSignal()
+    needToWaitForResultChanged = pyqtSignal()
+    errorCheckFinished = pyqtSignal()
+
+    @pyqtProperty(bool, notify = hasErrorUpdated)
+    def hasError(self) -> bool:
+        return self._has_errors
+
+    @pyqtProperty(bool, notify = needToWaitForResultChanged)
+    def needToWaitForResult(self) -> bool:
+        return self._need_to_check or self._check_in_progress
+
+    # Starts the error check timer to schedule a new error check.
+    def startErrorCheck(self, *args):
+        if not self._check_in_progress:
+            self._need_to_check = True
+            self.needToWaitForResultChanged.emit()
+        self._error_check_timer.start()
+
+    # This function is called by the timer to reschedule a new error check.
+    # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
+    # to notify the current check to stop and start a new one.
+    def _rescheduleCheck(self):
+        if self._check_in_progress and not self._need_to_check:
+            self._need_to_check = True
+            self.needToWaitForResultChanged.emit()
+            return
+
+        self._error_keys_in_progress = set()
+        self._need_to_check = False
+        self.needToWaitForResultChanged.emit()
+
+        global_stack = self._machine_manager.activeMachine
+        if global_stack is None:
+            Logger.log("i", "No active machine, nothing to check.")
+            return
+
+        # Populate the (stack, key) tuples to check
+        self._stacks_and_keys_to_check = deque()
+        for stack in [global_stack] + list(global_stack.extruders.values()):
+            for key in stack.getAllKeys():
+                self._stacks_and_keys_to_check.append((stack, key))
+
+        self._application.callLater(self._checkStack)
+        self._start_time = time.time()
+        Logger.log("d", "New error check scheduled.")
+
+    def _checkStack(self):
+        if self._need_to_check:
+            Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
+            self._check_in_progress = False
+            self._application.callLater(self.startErrorCheck)
+            return
+
+        self._check_in_progress = True
+
+        # If there is nothing to check any more, it means there is no error.
+        if not self._stacks_and_keys_to_check:
+            # Finish
+            self._setResult(False)
+            return
+
+        # Get the next stack and key to check
+        stack, key = self._stacks_and_keys_to_check.popleft()
+
+        enabled = stack.getProperty(key, "enabled")
+        if not enabled:
+            self._application.callLater(self._checkStack)
+            return
+
+        validation_state = stack.getProperty(key, "validationState")
+        if validation_state is None:
+            # Setting is not validated. This can happen if there is only a setting definition.
+            # We do need to validate it, because a setting definitions value can be set by a function, which could
+            # be an invalid setting.
+            definition = stack.getSettingDefinition(key)
+            validator_type = SettingDefinition.getValidatorForType(definition.type)
+            if validator_type:
+                validator = validator_type(key)
+                validation_state = validator(stack)
+        if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
+            # Finish
+            self._setResult(True)
+            return
+
+        # Schedule the check for the next key
+        self._application.callLater(self._checkStack)
+
+    def _setResult(self, result: bool):
+        if result != self._has_errors:
+            self._has_errors = result
+            self.hasErrorUpdated.emit()
+            self._machine_manager.stacksValidationChanged.emit()
+        self._need_to_check = False
+        self._check_in_progress = False
+        self.needToWaitForResultChanged.emit()
+        self.errorCheckFinished.emit()
+        Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time)

+ 5 - 24
cura/Settings/MachineManager.py

@@ -4,7 +4,7 @@
 import collections
 import time
 #Type hinting.
-from typing import Union, List, Dict, TYPE_CHECKING, Optional
+from typing import List, Dict, TYPE_CHECKING, Optional
 
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Signal import Signal
@@ -20,7 +20,6 @@ from UM.Logger import Logger
 from UM.Message import Message
 
 from UM.Settings.ContainerRegistry import ContainerRegistry
-from UM.Settings.InstanceContainer import InstanceContainer
 from UM.Settings.SettingFunction import SettingFunction
 from UM.Signal import postponeSignals, CompressTechnique
 
@@ -56,11 +55,6 @@ class MachineManager(QObject):
 
         self.machine_extruder_material_update_dict = collections.defaultdict(list)
 
-        self._error_check_timer = QTimer()
-        self._error_check_timer.setInterval(250)
-        self._error_check_timer.setSingleShot(True)
-        self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
-
         self._instance_container_timer = QTimer()
         self._instance_container_timer.setInterval(250)
         self._instance_container_timer.setSingleShot(True)
@@ -228,15 +222,6 @@ class MachineManager(QObject):
                 del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
 
         self.activeQualityGroupChanged.emit()
-        self._error_check_timer.start()
-
-    ##  Update self._stacks_valid according to _checkStacksForErrors and emit if change.
-    def _updateStacksHaveErrors(self) -> None:
-        old_stacks_have_errors = self._stacks_have_errors
-        self._stacks_have_errors = self._checkStacksHaveErrors()
-        if old_stacks_have_errors != self._stacks_have_errors:
-            self.stacksValidationChanged.emit()
-        Application.getInstance().stacksValidationFinished.emit()
 
     def _onActiveExtruderStackChanged(self) -> None:
         self.blurSettings.emit()  # Ensure no-one has focus.
@@ -256,8 +241,6 @@ class MachineManager(QObject):
 
         self.rootMaterialChanged.emit()
 
-        self._error_check_timer.start()
-
     def _onInstanceContainersChanged(self, container) -> None:
         self._instance_container_timer.start()
 
@@ -266,9 +249,6 @@ class MachineManager(QObject):
             # Notify UI items, such as the "changed" star in profile pull down menu.
             self.activeStackValueChanged.emit()
 
-        elif property_name == "validationState":
-            self._error_check_timer.start()
-
     ## Given a global_stack, make sure that it's all valid by searching for this quality group and applying it again
     def _initMachineState(self, global_stack):
         material_dict = {}
@@ -832,9 +812,10 @@ class MachineManager(QObject):
 
     ##  This will fire the propertiesChanged for all settings so they will be updated in the front-end
     def forceUpdateAllSettings(self):
-        property_names = ["value", "resolve"]
-        for setting_key in self._global_container_stack.getAllKeys():
-            self._global_container_stack.propertiesChanged.emit(setting_key, property_names)
+        with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
+            property_names = ["value", "resolve"]
+            for setting_key in self._global_container_stack.getAllKeys():
+                self._global_container_stack.propertiesChanged.emit(setting_key, property_names)
 
     @pyqtSlot(int, bool)
     def setExtruderEnabled(self, position: int, enabled) -> None:

+ 45 - 30
plugins/CuraEngineBackend/CuraEngineBackend.py

@@ -10,7 +10,6 @@ from UM.Logger import Logger
 from UM.Message import Message
 from UM.PluginRegistry import PluginRegistry
 from UM.Resources import Resources
-from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
 from UM.Platform import Platform
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Qt.Duration import DurationFormat
@@ -32,6 +31,7 @@ import Arcus
 from UM.i18n import i18nCatalog
 catalog = i18nCatalog("cura")
 
+
 class CuraEngineBackend(QObject, Backend):
 
     backendError = Signal()
@@ -62,23 +62,26 @@ class CuraEngineBackend(QObject, Backend):
                     default_engine_location = execpath
                     break
 
+        self._application = Application.getInstance()
+        self._multi_build_plate_model = None
+        self._machine_error_checker = None
+
         if not default_engine_location:
             raise EnvironmentError("Could not find CuraEngine")
 
-        Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
+        Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
 
         default_engine_location = os.path.abspath(default_engine_location)
         Preferences.getInstance().addPreference("backend/location", default_engine_location)
 
         # Workaround to disable layer view processing if layer view is not active.
         self._layer_view_active = False
-        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
-        Application.getInstance().getMultiBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
         self._onActiveViewChanged()
+
         self._stored_layer_data = []
         self._stored_optimized_layer_data = {}  # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
 
-        self._scene = Application.getInstance().getController().getScene()
+        self._scene = self._application.getController().getScene()
         self._scene.sceneChanged.connect(self._onSceneChanged)
 
         # Triggers for auto-slicing. Auto-slicing is triggered as follows:
@@ -86,20 +89,10 @@ class CuraEngineBackend(QObject, Backend):
         #  - whenever there is a value change, we start the timer
         #  - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
         #    auto-slicing timer when that error check is finished
-        #  If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
-        #  and only wait for the error check to be finished to start the auto-slicing timer again.
+        # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
+        # to start the auto-slicing timer again.
         #
         self._global_container_stack = None
-        Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
-        self._onGlobalStackChanged()
-
-        Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
-        # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
-        ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
-
-        # A flag indicating if an error check was scheduled
-        # If so, we will stop the auto-slice timer and start upon the error check
-        self._is_error_check_scheduled = False
 
         # Listeners for receiving messages from the back-end.
         self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
@@ -125,13 +118,6 @@ class CuraEngineBackend(QObject, Backend):
         self._last_num_objects = defaultdict(int)  # Count number of objects to see if there is something changed
         self._postponed_scene_change_sources = []  # scene change is postponed (by a tool)
 
-        self.backendQuit.connect(self._onBackendQuit)
-        self.backendConnected.connect(self._onBackendConnected)
-
-        # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
-        Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
-        Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
-
         self._slice_start_time = None
 
         Preferences.getInstance().addPreference("general/auto_slice", True)
@@ -146,6 +132,30 @@ class CuraEngineBackend(QObject, Backend):
         self.determineAutoSlicing()
         Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
 
+        self._application.initializationFinished.connect(self.initialize)
+
+    def initialize(self):
+        self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
+
+        self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
+        self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
+
+        self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
+        self._onGlobalStackChanged()
+
+        # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
+        ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
+
+        self.backendQuit.connect(self._onBackendQuit)
+        self.backendConnected.connect(self._onBackendConnected)
+
+        # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
+        self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
+        self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
+
+        self._machine_error_checker = self._application.getMachineErrorChecker()
+        self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
+
     ##  Terminate the engine process.
     #
     #   This function should terminate the engine process.
@@ -531,11 +541,9 @@ class CuraEngineBackend(QObject, Backend):
 
         elif property == "validationState":
             if self._use_timer:
-                self._is_error_check_scheduled = True
                 self._change_timer.stop()
 
     def _onStackErrorCheckFinished(self):
-        self._is_error_check_scheduled = False
         if not self._slicing and self._build_plates_to_be_sliced:
             self.needsSlicing()
             self._onChanged()
@@ -561,12 +569,15 @@ class CuraEngineBackend(QObject, Backend):
         self.processingProgress.emit(message.amount)
         self.backendStateChange.emit(BackendState.Processing)
 
-    # testing
     def _invokeSlice(self):
         if self._use_timer:
             # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
             # otherwise business as usual
-            if self._is_error_check_scheduled:
+            if self._machine_error_checker is None:
+                self._change_timer.stop()
+                return
+
+            if self._machine_error_checker.needToWaitForResult:
                 self._change_timer.stop()
             else:
                 self._change_timer.start()
@@ -632,7 +643,11 @@ class CuraEngineBackend(QObject, Backend):
         if self._use_timer:
             # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
             # otherwise business as usual
-            if self._is_error_check_scheduled:
+            if self._machine_error_checker is None:
+                self._change_timer.stop()
+                return
+
+            if self._machine_error_checker.needToWaitForResult:
                 self._change_timer.stop()
             else:
                 self._change_timer.start()
@@ -786,7 +801,7 @@ class CuraEngineBackend(QObject, Backend):
             self._change_timer.start()
 
     def _extruderChanged(self):
-        for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
+        for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
             if build_plate_number not in self._build_plates_to_be_sliced:
                 self._build_plates_to_be_sliced.append(build_plate_number)
         self._invokeSlice()

+ 0 - 2
resources/qml/Settings/SettingView.qml

@@ -374,8 +374,6 @@ Item
                     key: model.key ? model.key : ""
                     watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ]
                     storeIndex: 0
-                    // Due to the way setPropertyValue works, removeUnusedValue gives the correct output in case of resolve
-                    removeUnusedValue: model.resolve == undefined
                 }
 
                 Connections