# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from collections import deque from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty from UM.Application import Application from UM.Logger import Logger # # 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_to_check = None # a FIFO queue of stacks to check for errors self._keys_to_check = None # a FIFO queue of setting keys 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() # 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(300) 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 self._stacks_to_check = deque([global_stack] + list(global_stack.extruders.values())) self._keys_to_check = deque(global_stack.getAllKeys()) self._application.callLater(self._checkStack) Logger.log("d", "New error check scheduled.") def _checkStack(self): from UM.Settings.SettingDefinition import SettingDefinition from UM.Settings.Validator import ValidatorState 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_to_check or not self._keys_to_check: # Finish self._setResult(False) return stack = self._stacks_to_check[0] key = self._keys_to_check.popleft() # If there is no key left in this stack, check the next stack later. if not self._keys_to_check: if len(self._stacks_to_check) == 1: stacks = None keys = None else: stack = self._stacks_to_check.popleft() self._keys_to_check = deque(stack.getAllKeys()) 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", result)