def test_signal(): test = SignalReceiver() signal = Signal(type = Signal.Direct) signal.connect(test.slot) signal.emit() assert test.getEmitCount() == 1
def test_connectWhilePostponed(): test = SignalReceiver() signal = Signal(type=Signal.Direct) with postponeSignals(signal): signal.connect(test.slot) # This won't do anything, as we're postponing at the moment! signal.emit() assert test.getEmitCount() == 0 # The connection was never made, so we should get 0
def test_deepCopy(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) copied_signal = deepcopy(signal) copied_signal.emit() # Even though the original signal was not called, the copied one should have the same result assert test.getEmitCount() == 1
def test_signalWithFlameProfiler(): with patch("UM.Signal._recordSignalNames", MagicMock(return_value = True)): FlameProfiler.record_profile = True test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) signal.emit() assert test.getEmitCount() == 1 FlameProfiler.record_profile = False
def test_disconnectWhilePostponed(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) with postponeSignals(signal): signal.disconnect(test.slot) # This won't do anything, as we're postponing at the moment! signal.disconnectAll() # Same holds true for the disconnect all signal.emit() assert test.getEmitCount() == 1 # Despite attempting to disconnect, this didn't happen because of the postpone
def test_postponeEmitCompressPerParameterValue(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) with postponeSignals(signal, compress=CompressTechnique.CompressPerParameterValue): signal.emit("ZOMG") assert test.getEmitCount() == 0 # as long as we're in this context, nothing should happen! signal.emit("ZOMG") assert test.getEmitCount() == 0 signal.emit("BEEP") # We got 3 signal emits, but 2 of them were the same, so we end up with 2 unique emits. assert test.getEmitCount() == 2
def __init__(self) -> None: # A dict with the prepared replies, using the format {(http_method, url): reply} self.replies = {} # type: Dict[Tuple[str, str], MagicMock] self.request_bodies = {} # type: Dict[Tuple[str, str], bytes] # Signals used in the network manager. self.finished = Signal() self.authenticationRequired = Signal()
def test_doubleSignalWithFlameProfiler(): FlameProfiler.record_profile = True test = SignalReceiver() signal = Signal(type=Signal.Direct) signal2 = Signal(type=Signal.Direct) signal.connect(test.slot) signal2.connect(signal) signal2.emit() assert test.getEmitCount() == 1 FlameProfiler.record_profile = False
def test_postponeEmitCompressSingle(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) with postponeSignals(signal, compress=CompressTechnique.CompressSingle): signal.emit() assert test.getEmitCount() == 0 # as long as we're in this context, nothing should happen! signal.emit() assert test.getEmitCount() == 0 assert test.getEmitCount() == 1
class LicensePresenter(QObject): """Presents licenses for a set of packages for the user to accept or reject. Call present() exactly once to show a licenseDialog for a set of packages Before presenting another set of licenses, create a new instance using resetCopy(). licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages. """ def __init__(self, app: CuraApplication) -> None: super().__init__() self._presented = False """Whether present() has been called and state is expected to be initialized""" self._catalog = i18nCatalog("cura") self._dialog = None # type: Optional[QObject] self._package_manager = app.getPackageManager() # type: PackageManager # Emits List[Dict[str, [Any]] containing for example # [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }] self.licenseAnswers = Signal() self._current_package_idx = 0 self._package_models = [] # type: List[Dict] decline_button_text = self._catalog.i18nc( "@button", "Decline and remove from account") self._license_model = LicenseModel( decline_button_text=decline_button_text) # type: LicenseModel self._page_count = 0 self._app = app self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml" ## Show a license dialog for multiple packages where users can read a license and accept or decline them # \param plugin_path: Root directory of the Toolbox plugin # \param packages: Dict[package id, file path] def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: if self._presented: Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) return path = os.path.join(plugin_path, self._compatibility_dialog_path) self._initState(packages) if self._page_count == 0: self.licenseAnswers.emit(self._package_models) return if self._dialog is None: context_properties = { "catalog": self._catalog, "licenseModel": self._license_model, "handler": self } self._dialog = self._app.createQmlComponent( path, context_properties) self._presentCurrentPackage() self._presented = True def resetCopy(self) -> "LicensePresenter": """Clean up and return a new copy with the same settings such as app""" if self._dialog: self._dialog.close() self.licenseAnswers.disconnectAll() return LicensePresenter(self._app) @pyqtSlot() def onLicenseAccepted(self) -> None: self._package_models[self._current_package_idx]["accepted"] = True self._checkNextPage() @pyqtSlot() def onLicenseDeclined(self) -> None: self._package_models[self._current_package_idx]["accepted"] = False self._checkNextPage() def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None: implicitly_accepted_count = 0 for package_id, item in packages.items(): item["package_id"] = package_id item["licence_content"] = self._package_manager.getPackageLicense( item["package_path"]) if item["licence_content"] is None: # Implicitly accept when there is no license item["accepted"] = True implicitly_accepted_count = implicitly_accepted_count + 1 self._package_models.append(item) else: item["accepted"] = None #: None: no answer yet # When presenting the packages, we want to show packages which have a license first. # In fact, we don't want to show the others at all because they are implicitly accepted self._package_models.insert(0, item) CuraApplication.getInstance().processEvents() self._page_count = len( self._package_models) - implicitly_accepted_count self._license_model.setPageCount(self._page_count) def _presentCurrentPackage(self) -> None: package_model = self._package_models[self._current_package_idx] package_info = self._package_manager.getPackageInfo( package_model["package_path"]) self._license_model.setCurrentPageIdx(self._current_package_idx) self._license_model.setPackageName(package_info["display_name"]) self._license_model.setIconUrl(package_model["icon_url"]) self._license_model.setLicenseText(package_model["licence_content"]) if self._dialog: self._dialog.open() # Does nothing if already open def _checkNextPage(self) -> None: if self._current_package_idx + 1 < self._page_count: self._current_package_idx += 1 self._presentCurrentPackage() else: if self._dialog: self._dialog.close() self.licenseAnswers.emit(self._package_models)
class ContainerStack(QObject, ContainerInterface, PluginObject): Version = 4 # type: int ## Constructor # # \param stack_id A unique, machine readable/writable ID. def __init__(self, stack_id: str) -> None: super().__init__() QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership) self._metadata = { "id": stack_id, "name": stack_id, "version": self.Version, "container_type": ContainerStack } #type: Dict[str, Any] self._containers = [] # type: List[ContainerInterface] self._next_stack = None # type: Optional[ContainerStack] self._read_only = False # type: bool self._dirty = True # type: bool self._path = "" # type: str self._postponed_emits = [] #type: List[Tuple[Signal, ContainerInterface]] # gets filled with 2-tuples: signal, signal_argument(s) self._property_changes = {} #type: Dict[str, Set[str]] self._emit_property_changed_queued = False # type: bool ## For pickle support def __getnewargs__(self) -> Tuple[str]: return (self.getId(),) ## For pickle support def __getstate__(self) -> Dict[str, Any]: return self.__dict__ ## For pickle support def __setstate__(self, state: Dict[str, Any]) -> None: self.__dict__.update(state) ## \copydoc ContainerInterface::getId # # Reimplemented from ContainerInterface def getId(self) -> str: return cast(str, self._metadata["id"]) id = pyqtProperty(str, fget = getId, constant = True) ## \copydoc ContainerInterface::getName # # Reimplemented from ContainerInterface def getName(self) -> str: return str(self._metadata["name"]) ## Set the name of this stack. # # \param name \type{string} The new name of the stack. def setName(self, name: str) -> None: if name != self.getName(): self._metadata["name"] = name self.nameChanged.emit() self.metaDataChanged.emit(self) ## Emitted whenever the name of this stack changes. nameChanged = pyqtSignal() name = pyqtProperty(str, fget = getName, fset = setName, notify = nameChanged) ## \copydoc ContainerInterface::isReadOnly # # Reimplemented from ContainerInterface def isReadOnly(self) -> bool: return self._read_only def setReadOnly(self, read_only: bool) -> None: if read_only != self._read_only: self._read_only = read_only self.readOnlyChanged.emit() readOnlyChanged = pyqtSignal() readOnly = pyqtProperty(bool, fget = isReadOnly, fset = setReadOnly, notify = readOnlyChanged) ## \copydoc ContainerInterface::getMetaData # # Reimplemented from ContainerInterface def getMetaData(self) -> Dict[str, Any]: return self._metadata ## Set the complete set of metadata def setMetaData(self, meta_data: Dict[str, Any]) -> None: if meta_data == self.getMetaData(): return #Unnecessary. #We'll fill a temporary dictionary with all the required metadata and overwrite it with the new metadata. #This way it is ensured that at least the required metadata is still there. self._metadata = { "id": self.getId(), "name": self.getName(), "version": self.getMetaData().get("version", 0), "container_type": ContainerStack } self._metadata.update(meta_data) self.metaDataChanged.emit(self) metaDataChanged = pyqtSignal(QObject) metaData = pyqtProperty("QVariantMap", fget = getMetaData, fset = setMetaData, notify = metaDataChanged) ## \copydoc ContainerInterface::getMetaDataEntry # # Reimplemented from ContainerInterface def getMetaDataEntry(self, entry: str, default = None) -> Any: value = self._metadata.get(entry, None) if value is None: for container in self._containers: value = container.getMetaDataEntry(entry, None) if value is not None: break if value is None: return default else: return value def setMetaDataEntry(self, key: str, value: Any) -> None: if key not in self._metadata or self._metadata[key] != value: self._metadata[key] = value self._dirty = True self.metaDataChanged.emit(self) def removeMetaDataEntry(self, key: str) -> None: if key in self._metadata: del self._metadata[key] self.metaDataChanged.emit(self) def isDirty(self) -> bool: return self._dirty def setDirty(self, dirty: bool) -> None: self._dirty = dirty containersChanged = Signal() ## \copydoc ContainerInterface::getProperty # # Reimplemented from ContainerInterface. # # getProperty will start at the top of the stack and try to get the property # specified. If that container returns no value, the next container on the # stack will be tried and so on until the bottom of the stack is reached. # If a next stack is defined for this stack it will then try to get the # value from that stack. If no next stack is defined, None will be returned. # # Note that if the property value is a function, this method will return the # result of evaluating that property with the current stack. If you need the # actual function, use getRawProperty() def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: value = self.getRawProperty(key, property_name, context = context) if isinstance(value, SettingFunction): if context is not None: context.pushContainer(self) value = value(self, context) if context is not None: context.popContainer() return value ## Retrieve a property of a setting by key and property name. # # This method does the same as getProperty() except it does not perform any # special handling of the result, instead the raw stored value is returned. # # \param key The key to get the property value of. # \param property_name The name of the property to get the value of. # \param use_next True if the value should be retrieved from the next # stack if not found in this stack. False if not. # \param skip_until_container A container ID to skip to. If set, it will # be as if all containers above the specified container are empty. If the # container is not in the stack, it'll try to find it in the next stack. # # \return The raw property value of the property, or None if not found. Note that # the value might be a SettingFunction instance. # def getRawProperty(self, key: str, property_name: str, *, context: Optional[PropertyEvaluationContext] = None, use_next: bool = True, skip_until_container: Optional[ContainerInterface] = None) -> Any: containers = self._containers if context is not None: # if context is provided, check if there is any container that needs to be skipped. start_index = context.context.get("evaluate_from_container_index", 0) if start_index >= len(self._containers): return None containers = self._containers[start_index:] for container in containers: if skip_until_container and container.getId() != skip_until_container: continue #Skip. skip_until_container = None #When we find the container, stop skipping. value = container.getProperty(key, property_name, context) if value is not None: return value if self._next_stack and use_next: return self._next_stack.getRawProperty(key, property_name, context = context, use_next = use_next, skip_until_container = skip_until_container) else: return None ## \copydoc ContainerInterface::hasProperty # # Reimplemented from ContainerInterface. # # hasProperty will check if any of the containers in the stack has the # specified property. If it does, it stops and returns True. If it gets to # the end of the stack, it returns False. def hasProperty(self, key: str, property_name: str) -> bool: for container in self._containers: if container.hasProperty(key, property_name): return True if self._next_stack: return self._next_stack.hasProperty(key, property_name) return False # NOTE: we make propertyChanged and propertiesChanged as queued signals because otherwise, the emits in # _emitCollectedPropertyChanges() will be direct calls which modify the dict we are iterating over, and then # everything crashes. propertyChanged = Signal(Signal.Queued) propertiesChanged = Signal(Signal.Queued) ## \copydoc ContainerInterface::serialize # # Reimplemented from ContainerInterface # # TODO: Expand documentation here, include the fact that this should _not_ include all containers def serialize(self, ignored_metadata_keys: Optional[set] = None) -> str: parser = configparser.ConfigParser(interpolation = None, empty_lines_in_values = False) parser["general"] = {} parser["general"]["version"] = str(self._metadata["version"]) parser["general"]["name"] = str(self.getName()) parser["general"]["id"] = str(self.getId()) if ignored_metadata_keys is None: ignored_metadata_keys = set() ignored_metadata_keys |= {"id", "name", "version", "container_type"} parser["metadata"] = {} for key, value in self._metadata.items(): # only serialize the data that's not in the ignore list if key not in ignored_metadata_keys: parser["metadata"][key] = str(value) parser.add_section("containers") for index in range(len(self._containers)): parser["containers"][str(index)] = str(self._containers[index].getId()) stream = io.StringIO() parser.write(stream) return stream.getvalue() ## Deserializes the given data and checks if the required fields are present. # # The profile upgrading code depends on information such as "configuration_type" and "version", which come from # the serialized data. Due to legacy problem, those data may not be available if it comes from an ancient Cura. @classmethod def _readAndValidateSerialized(cls, serialized: str) -> configparser.ConfigParser: parser = configparser.ConfigParser(interpolation = None, empty_lines_in_values=False) parser.read_string(serialized) if "general" not in parser or any(pn not in parser["general"] for pn in ("version", "name", "id")): raise InvalidContainerStackError("Missing required section 'general' or 'version' property") return parser @classmethod def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]: configuration_type = None try: parser = cls._readAndValidateSerialized(serialized) configuration_type = parser["metadata"]["type"] except InvalidContainerStackError as icse: raise icse except Exception as e: Logger.log("e", "Could not get configuration type: %s", e) return configuration_type @classmethod def getVersionFromSerialized(cls, serialized: str) -> Optional[int]: configuration_type = cls.getConfigurationTypeFromSerialized(serialized) if not configuration_type: Logger.log("d", "Could not get type from serialized.") return None # Get version version = None try: from UM.VersionUpgradeManager import VersionUpgradeManager version = VersionUpgradeManager.getInstance().getFileVersion(configuration_type, serialized) except Exception as e: Logger.log("d", "Could not get version from serialized: %s", e) return version ## \copydoc ContainerInterface::deserialize # # Reimplemented from ContainerInterface # # TODO: Expand documentation here, include the fact that this should _not_ include all containers def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: # Update the serialized data first serialized = super().deserialize(serialized, file_name) parser = self._readAndValidateSerialized(serialized) if parser.getint("general", "version") != self.Version: raise IncorrectVersionError() # Clear all data before starting. for container in self._containers: container.propertyChanged.disconnect(self._collectPropertyChanges) self._containers = [] self._metadata = {} if "metadata" in parser: self._metadata = dict(parser["metadata"]) self._metadata["id"] = parser["general"]["id"] self._metadata["name"] = parser["general"].get("name", self.getId()) self._metadata["version"] = self.Version # Guaranteed to be equal to what's in the container. See above. self._metadata["container_type"] = ContainerStack if "containers" in parser: for index, container_id in parser.items("containers"): containers = _containerRegistry.findContainers(id = container_id) if containers: containers[0].propertyChanged.connect(self._collectPropertyChanges) self._containers.append(containers[0]) else: self._containers.append(_containerRegistry.getEmptyInstanceContainer()) ConfigurationErrorMessage.getInstance().addFaultyContainers(container_id, self.getId()) Logger.log("e", "When trying to deserialize %s, we received an unknown container ID (%s)" % (self.getId(), container_id)) raise ContainerFormatError("When trying to deserialize %s, we received an unknown container ID (%s)" % (self.getId(), container_id)) elif parser.has_option("general", "containers"): # Backward compatibility with 2.3.1: The containers used to be saved in a single comma-separated list. container_string = parser["general"].get("containers", "") Logger.log("d", "While deserializing, we got the following container string: %s", container_string) container_id_list = container_string.split(",") for container_id in container_id_list: if container_id != "": containers = _containerRegistry.findContainers(id = container_id) if containers: containers[0].propertyChanged.connect(self._collectPropertyChanges) self._containers.append(containers[0]) else: self._containers.append(_containerRegistry.getEmptyInstanceContainer()) ConfigurationErrorMessage.getInstance().addFaultyContainers(container_id, self.getId()) Logger.log("e", "When trying to deserialize %s, we received an unknown container ID (%s)" % (self.getId(), container_id)) raise ContainerFormatError("When trying to deserialize %s, we received an unknown container ID (%s)" % (self.getId(), container_id)) ## TODO; Deserialize the containers. return serialized ## Gets the metadata of a container stack from a serialised format. # # This parses the entire CFG document and only extracts the metadata from # it. # # \param serialized A CFG document, serialised as a string. # \param container_id The ID of the container that we're getting the # metadata of, as obtained from the file name. # \return A dictionary of metadata that was in the CFG document as a # singleton list. If anything went wrong, this returns an empty list # instead. @classmethod def deserializeMetadata(cls, serialized: str, container_id: str) -> List[Dict[str, Any]]: serialized = cls._updateSerialized(serialized) # Update to most recent version. parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) metadata = { "id": container_id, "container_type": ContainerStack } try: metadata["name"] = parser["general"]["name"] metadata["version"] = parser["general"]["version"] except KeyError as e: # One of the keys or the General section itself is missing. raise InvalidContainerStackError("Missing required fields: {error_msg}".format(error_msg = str(e))) if "metadata" in parser: metadata.update(parser["metadata"]) return [metadata] ## Get all keys known to this container stack. # # In combination with getProperty(), you can obtain the current property # values of all settings. # # \return A set of all setting keys in this container stack. def getAllKeys(self) -> Set[str]: keys = set() # type: Set[str] definition_containers = [container for container in self.getContainers() if container.__class__ == DefinitionContainer] #To get all keys, get all definitions from all definition containers. for definition_container in cast(List[DefinitionContainer], definition_containers): keys |= definition_container.getAllKeys() if self._next_stack: keys |= self._next_stack.getAllKeys() return keys ## Get a list of all containers in this stack. # # Note that it returns a shallow copy of the container list, as it's only allowed to change the order or entries # in this list by the proper functions. # \return \type{list} A list of all containers in this stack. def getContainers(self) -> List[ContainerInterface]: return self._containers[:] def getContainerIndex(self, container: ContainerInterface) -> int: return self._containers.index(container) ## Get a container by index. # # \param index The index of the container to get. # # \return The container at the specified index. # # \exception IndexError Raised when the specified index is out of bounds. def getContainer(self, index: int) -> ContainerInterface: if index < 0: raise IndexError return self._containers[index] ## Get the container at the top of the stack. # # This is a convenience method that will always return the top of the stack. # # \return The container at the top of the stack, or None if no containers have been added. def getTop(self) -> Optional[ContainerInterface]: if self._containers: return self._containers[0] return None ## Get the container at the bottom of the stack. # # This is a convenience method that will always return the bottom of the stack. # # \return The container at the bottom of the stack, or None if no containers have been added. def getBottom(self) -> Optional[ContainerInterface]: if self._containers: return self._containers[-1] return None ## \copydoc ContainerInterface::getPath. # # Reimplemented from ContainerInterface def getPath(self) -> str: return self._path ## \copydoc ContainerInterface::setPath # # Reimplemented from ContainerInterface def setPath(self, path: str) -> None: self._path = path ## Get the SettingDefinition object for a specified key def getSettingDefinition(self, key: str) -> Optional[SettingDefinition]: for container in self._containers: if not isinstance(container, DefinitionContainer): continue settings = container.findDefinitions(key = key) if settings: return settings[0] if self._next_stack: return self._next_stack.getSettingDefinition(key) else: return None ## Find a container matching certain criteria. # # \param criteria A dictionary containing key and value pairs that need to # match the container. Note that the value of "*" can be used as a wild # card. This will ensure that any container that has the specified key in # the meta data is found. # \param container_type An optional type of container to filter on. # \return The first container that matches the filter criteria or None if not found. @UM.FlameProfiler.profile def findContainer(self, criteria: Dict[str, Any] = None, container_type: type = None, **kwargs: Any) -> Optional[ContainerInterface]: if not criteria and kwargs: criteria = kwargs elif criteria is None: criteria = {} for container in self._containers: meta_data = container.getMetaData() match = container.__class__ == container_type or container_type is None for key in criteria: if not match: break try: if meta_data[key] == criteria[key] or criteria[key] == "*": continue else: match = False break except KeyError: match = False break if match: return container return None ## Add a container to the top of the stack. # # \param container The container to add to the stack. def addContainer(self, container: ContainerInterface) -> None: self.insertContainer(0, container) ## Insert a container into the stack. # # \param index The index of to insert the container at. # A negative index counts from the bottom # \param container The container to add to the stack. def insertContainer(self, index: int, container: ContainerInterface) -> None: if container is self: raise Exception("Unable to add stack to itself.") container.propertyChanged.connect(self._collectPropertyChanges) self._containers.insert(index, container) self.containersChanged.emit(container) ## Replace a container in the stack. # # \param index \type{int} The index of the container to replace. # \param container The container to replace the existing entry with. # \param postpone_emit During stack manipulation you may want to emit later. # # \exception IndexError Raised when the specified index is out of bounds. # \exception Exception when trying to replace container ContainerStack. def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: if index < 0: raise IndexError if container is self: raise Exception("Unable to replace container with ContainerStack (self) ") self._containers[index].propertyChanged.disconnect(self._collectPropertyChanges) container.propertyChanged.connect(self._collectPropertyChanges) self._containers[index] = container if postpone_emit: # send it using sendPostponedEmits self._postponed_emits.append((self.containersChanged, container)) else: self.containersChanged.emit(container) ## Remove a container from the stack. # # \param index \type{int} The index of the container to remove. # # \exception IndexError Raised when the specified index is out of bounds. def removeContainer(self, index: int = 0) -> None: if index < 0: raise IndexError try: container = self._containers[index] container.propertyChanged.disconnect(self._collectPropertyChanges) del self._containers[index] self.containersChanged.emit(container) except TypeError: raise IndexError("Can't delete container with index %s" % index) ## Get the next stack # # The next stack is the stack that is searched for a setting value if the # bottom of the stack is reached when searching for a value. # # \return \type{ContainerStack} The next stack or None if not set. def getNextStack(self) -> Optional["ContainerStack"]: return self._next_stack ## Set the next stack # # \param stack \type{ContainerStack} The next stack to set. Can be None. # Raises Exception when trying to set itself as next stack (to prevent infinite loops) # \sa getNextStack def setNextStack(self, stack: "ContainerStack", connect_signals: bool = True) -> None: if self is stack: raise Exception("Next stack can not be itself") if self._next_stack == stack: return if self._next_stack: self._next_stack.propertyChanged.disconnect(self._collectPropertyChanges) self.containersChanged.disconnect(self._next_stack.containersChanged) self._next_stack = stack if self._next_stack and connect_signals: self._next_stack.propertyChanged.connect(self._collectPropertyChanges) self.containersChanged.connect(self._next_stack.containersChanged) ## Send postponed emits # These emits are collected from the option postpone_emit. # Note: the option can be implemented for all functions modifying the stack. def sendPostponedEmits(self) -> None: while self._postponed_emits: signal, signal_arg = self._postponed_emits.pop(0) signal.emit(signal_arg) ## Check if the container stack has errors @UM.FlameProfiler.profile def hasErrors(self) -> bool: for key in self.getAllKeys(): enabled = self.getProperty(key, "enabled") if not enabled: continue validation_state = self.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 defintions value can be set by a function, which could # be an invalid setting. definition = cast(SettingDefinition, self.getSettingDefinition(key)) validator_type = SettingDefinition.getValidatorForType(definition.type) if validator_type: validator = validator_type(key) validation_state = validator(self) if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): return True return False ## Get all the keys that are in an error state in this stack @UM.FlameProfiler.profile def getErrorKeys(self) -> List[str]: error_keys = [] for key in self.getAllKeys(): validation_state = self.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 defintions value can be set by a function, which could # be an invalid setting. definition = cast(SettingDefinition, self.getSettingDefinition(key)) validator_type = SettingDefinition.getValidatorForType(definition.type) if validator_type: validator = validator_type(key) validation_state = validator(self) if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): error_keys.append(key) return error_keys # protected: # Gather up all signal emissions and delay their emit until the next time the event # loop can run. This prevents us from sending the same change signal multiple times. # In addition, it allows us to emit a single signal that reports all properties that # have changed. def _collectPropertyChanges(self, key: str, property_name: str) -> None: if key not in self._property_changes: self._property_changes[key] = set() self._property_changes[key].add(property_name) if not self._emit_property_changed_queued: from UM.Application import Application Application.getInstance().callLater(self._emitCollectedPropertyChanges) self._emit_property_changed_queued = True # Perform the emission of the change signals that were collected in a previous step. def _emitCollectedPropertyChanges(self) -> None: for key, property_names in self._property_changes.items(): self.propertiesChanged.emit(key, property_names) for property_name in property_names: self.propertyChanged.emit(key, property_name) self._property_changes = {} self._emit_property_changed_queued = False def __str__(self) -> str: return "%s(%s)" % (type(self).__name__, self.getId())
class QtApplication(QApplication, Application): def __init__(self, **kwargs): plugin_path = "" if sys.platform == "win32": if hasattr(sys, "frozen"): plugin_path = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "PyQt5", "plugins") Logger.log("i", "Adding QT5 plugin path: %s" % (plugin_path)) QCoreApplication.addLibraryPath(plugin_path) else: import site for dir in site.getsitepackages(): QCoreApplication.addLibraryPath(os.path.join(dir, "PyQt5", "plugins")) elif sys.platform == "darwin": plugin_path = os.path.join(Application.getInstallPrefix(), "Resources", "plugins") if plugin_path: Logger.log("i", "Adding QT5 plugin path: %s" % (plugin_path)) QCoreApplication.addLibraryPath(plugin_path) os.environ["QSG_RENDER_LOOP"] = "basic" super().__init__(sys.argv, **kwargs) self._plugins_loaded = False #Used to determine when it's safe to use the plug-ins. self._main_qml = "main.qml" self._engine = None self._renderer = None self._main_window = None self._theme = None self._shutting_down = False self._qml_import_paths = [] self._qml_import_paths.append(os.path.join(os.path.dirname(sys.executable), "qml")) self._qml_import_paths.append(os.path.join(Application.getInstallPrefix(), "Resources", "qml")) self.setAttribute(Qt.AA_UseDesktopOpenGL) try: self._splash = self._createSplashScreen() except FileNotFoundError: self._splash = None else: self._splash.show() self.processEvents() signal.signal(signal.SIGINT, signal.SIG_DFL) # This is done here as a lot of plugins require a correct gl context. If you want to change the framework, # these checks need to be done in your <framework>Application.py class __init__(). i18n_catalog = i18nCatalog("uranium") self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading plugins...")) self._loadPlugins() self.parseCommandLine() Logger.log("i", "Command line arguments: %s", self._parsed_command_line) self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins()) self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Updating configuration...")) upgraded = UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().upgrade() if upgraded: preferences = UM.Preferences.getInstance() #Preferences might have changed. Load them again. #Note that the language can't be updated, so that will always revert to English. try: preferences.readFromFile(Resources.getPath(Resources.Preferences, self._application_name + ".cfg")) except FileNotFoundError: pass self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading preferences...")) try: file = Resources.getPath(Resources.Preferences, self.getApplicationName() + ".cfg") Preferences.getInstance().readFromFile(file) except FileNotFoundError: pass def run(self): pass def hideMessage(self, message): with self._message_lock: if message in self._visible_messages: self._visible_messages.remove(message) self.visibleMessageRemoved.emit(message) def showMessage(self, message): with self._message_lock: if message not in self._visible_messages: self._visible_messages.append(message) message.setTimer(QTimer()) self.visibleMessageAdded.emit(message) def setMainQml(self, path): self._main_qml = path def initializeEngine(self): # TODO: Document native/qml import trickery Bindings.register() self._engine = QQmlApplicationEngine() for path in self._qml_import_paths: self._engine.addImportPath(path) if not hasattr(sys, "frozen"): self._engine.addImportPath(os.path.join(os.path.dirname(__file__), "qml")) self._engine.rootContext().setContextProperty("QT_VERSION_STR", QT_VERSION_STR) self._engine.rootContext().setContextProperty("screenScaleFactor", self._screenScaleFactor()) self.registerObjects(self._engine) self._engine.load(self._main_qml) self.engineCreatedSignal.emit() engineCreatedSignal = Signal() def isShuttingDown(self): return self._shutting_down def registerObjects(self, engine): pass def getRenderer(self): if not self._renderer: self._renderer = QtRenderer() return self._renderer @classmethod def addCommandLineOptions(self, parser): parser.add_argument("--disable-textures", dest="disable-textures", action="store_true", default=False, help="Disable Qt texture loading as a workaround for certain crashes.") # Overridden from QApplication::setApplicationName to call our internal setApplicationName def setApplicationName(self, name): Application.setApplicationName(self, name) mainWindowChanged = Signal() def getMainWindow(self): return self._main_window def setMainWindow(self, window): if window != self._main_window: self._main_window = window self.mainWindowChanged.emit() def getTheme(self, *args): if self._theme is None: if self._engine is None: Logger.log("e", "The theme cannot be accessed before the engine is initialised") return None self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(self._engine) return self._theme # Handle a function that should be called later. def functionEvent(self, event): e = _QtFunctionEvent(event) QCoreApplication.postEvent(self, e) # Handle Qt events def event(self, event): if event.type() == _QtFunctionEvent.QtFunctionEvent: event._function_event.call() return True return super().event(event) def windowClosed(self): Logger.log("d", "Shutting down %s", self.getApplicationName()) self._shutting_down = True try: Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, self.getApplicationName() + ".cfg")) except Exception as e: Logger.log("e", "Exception while saving preferences: %s", repr(e)) try: self.applicationShuttingDown.emit() except Exception as e: Logger.log("e", "Exception while emitting shutdown signal: %s", repr(e)) try: self.getBackend().close() except Exception as e: Logger.log("e", "Exception while closing backend: %s", repr(e)) self.quit() ## Load a Qt translation catalog. # # This method will locate, load and install a Qt message catalog that can be used # by Qt's translation system, like qsTr() in QML files. # # \param file The file name to load, without extension. It will be searched for in # the i18nLocation Resources directory. If it can not be found a warning # will be logged but no error will be thrown. # \param language The language to load translations for. This can be any valid language code # or 'default' in which case the language is looked up based on system locale. # If the specified language can not be found, this method will fall back to # loading the english translations file. # # \note When `language` is `default`, the language to load can be changed with the # environment variable "LANGUAGE". def loadQtTranslation(self, file, language = "default"): #TODO Add support for specifying a language from preferences path = None if language == "default": path = self._getDefaultLanguage(file) else: path = Resources.getPath(Resources.i18n, language, "LC_MESSAGES", file + ".qm") # If all else fails, fall back to english. if not path: Logger.log("w", "Could not find any translations matching {0} for file {1}, falling back to english".format(language, file)) try: path = Resources.getPath(Resources.i18n, "en", "LC_MESSAGES", file + ".qm") except FileNotFoundError: Logger.log("w", "Could not find English translations for file {0}. Switching to developer english.".format(file)) return translator = QTranslator() if not translator.load(path): Logger.log("e", "Unable to load translations %s", file) return # Store a reference to the translator. # This prevents the translator from being destroyed before Qt has a chance to use it. self._translators[file] = translator # Finally, install the translator so Qt can use it. self.installTranslator(translator) ## Display text on the splash screen. def showSplashMessage(self, message): if self._splash: self._splash.showMessage(message , Qt.AlignHCenter | Qt.AlignVCenter) self.processEvents() ## Close the splash screen after the application has started. def closeSplash(self): if self._splash: self._splash.close() self._splash = None def _createSplashScreen(self): return QSplashScreen(QPixmap(Resources.getPath(Resources.Images, self.getApplicationName() + ".png"))) def _screenScaleFactor(self): physical_dpi = QGuiApplication.primaryScreen().physicalDotsPerInch() # Typically 'normal' screens have a DPI around 96. Modern high DPI screens are up around 220. # We scale the low DPI screens with a traditional 1, and double the high DPI ones. return 1.0 if physical_dpi < 150 else 2.0 def _getDefaultLanguage(self, file): # If we have a language override set in the environment, try and use that. lang = os.getenv("URANIUM_LANGUAGE") if lang: try: return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES", file + ".qm") except FileNotFoundError: pass # Else, try and get the current language from preferences lang = Preferences.getInstance().getValue("general/language") if lang: try: return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES", file + ".qm") except FileNotFoundError: pass # If none of those are set, try to use the environment's LANGUAGE variable. lang = os.getenv("LANGUAGE") if lang: try: return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES", file + ".qm") except FileNotFoundError: pass # If looking up the language from the enviroment or preferences fails, try and use Qt's system locale instead. locale = QLocale.system() # First, try and find a directory for any of the provided languages for lang in locale.uiLanguages(): try: return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES", file + ".qm") except FileNotFoundError: pass # If that fails, see if we can extract a language "class" from the # preferred language. This will turn "en-GB" into "en" for example. lang = locale.uiLanguages()[0] lang = lang[0:lang.find("-")] try: return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES", file + ".qm") except FileNotFoundError: pass return None
class Application: """Central object responsible for running the main event loop and creating other central objects. The Application object is a central object for accessing other important objects. It is also responsible for starting the main event loop. It is passed on to plugins so it can be easily used to access objects required for those plugins. """ def __init__(self, name: str, version: str, api_version: str, app_display_name: str = "", build_type: str = "", is_debug_mode: bool = False, **kwargs) -> None: """Init method :param name: :type{string} The name of the application. :param version: :type{string} Version, formatted as major.minor.rev :param build_type: Additional version info on the type of build this is, such as "master". :param is_debug_mode: Whether to run in debug mode. """ if Application.__instance is not None: raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) Application.__instance = self super().__init__() # Call super to make multiple inheritance work. self._api_version = Version(api_version) # type: Version self._app_name = name # type: str self._app_display_name = app_display_name if app_display_name else name # type: str self._version = version # type: str self._build_type = build_type # type: str self._is_debug_mode = is_debug_mode # type: bool self._is_headless = False # type: bool self._use_external_backend = False # type: bool self._just_updated_from_old_version = False # type: bool self._config_lock_filename = "{name}.lock".format(name = self._app_name) # type: str self._cli_args = None # type: argparse.Namespace self._cli_parser = argparse.ArgumentParser(prog = self._app_name, add_help = False) # type: argparse.ArgumentParser self._main_thread = threading.current_thread() # type: threading.Thread self.default_theme = self._app_name # type: str # Default theme is the application name self._default_language = "en_US" # type: str self.change_log_url: str = "https://github.com/Ultimaker/Uranium" # Where to find a more detailed description of the recent updates. self.beta_change_log_url: str = "https://github.com/Ultimaker/Uranium" # Where to find a more detailed description of proposed updates. self._preferences_filename = None # type: str self._preferences = None # type: Preferences self._extensions = [] # type: List[Extension] self._file_providers = [] # type: List[FileProvider] self._required_plugins = [] # type: List[str] self._package_manager_class = PackageManager # type: type self._package_manager = None # type: PackageManager self._plugin_registry = None # type: PluginRegistry self._container_registry_class = ContainerRegistry # type: type self._container_registry = None # type: ContainerRegistry self._global_container_stack = None # type: Optional[ContainerStack] self._file_provider_model = FileProviderModel(application = self) # type: Optional[FileProviderModel] self._controller = None # type: Controller self._backend = None # type: Backend self._output_device_manager = None # type: OutputDeviceManager self._operation_stack = None # type: OperationStack self._visible_messages = [] # type: List[Message] self._message_lock = threading.Lock() # type: threading.Lock self._app_install_dir = self.getInstallPrefix() # type: str # Intended for keeping plugin workspace metadata that is going to be saved in and retrieved from workspace files. # When the workspace is stored, all workspace readers will need to ensure that the workspace metadata is correctly # stored to the output file. The same also holds when loading a workspace; the existing data will be cleared # and replaced with the data recovered from the file (if any). self._workspace_metadata_storage = WorkspaceMetadataStorage() # type: WorkspaceMetadataStorage # Intended for keeping plugin workspace information that is only temporary. The information added in this structure # is NOT saved to and retrieved from workspace files. self._current_workspace_information = WorkspaceMetadataStorage() # type: WorkspaceMetadataStorage def getAPIVersion(self) -> "Version": return self._api_version def getWorkspaceMetadataStorage(self) -> WorkspaceMetadataStorage: return self._workspace_metadata_storage def getCurrentWorkspaceInformation(self) -> WorkspaceMetadataStorage: return self._current_workspace_information # Adds the command line options that can be parsed by the command line parser. # Can be overridden to add additional command line options to the parser. def addCommandLineOptions(self) -> None: self._cli_parser.add_argument("--version", action = "version", version = "%(prog)s version: {0}".format(self._version)) self._cli_parser.add_argument("--external-backend", action = "store_true", default = False, help = "Use an externally started backend instead of starting it automatically. This is a debug feature to make it possible to run the engine with debug options enabled.") self._cli_parser.add_argument('--headless', action = 'store_true', default = False, help = "Hides all GUI elements.") self._cli_parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.") def parseCliOptions(self) -> None: self._cli_args = self._cli_parser.parse_args() self._is_headless = self._cli_args.headless self._is_debug_mode = self._cli_args.debug or self._is_debug_mode self._use_external_backend = self._cli_args.external_backend # Performs initialization that must be done before start. def initialize(self) -> None: Logger.log("d", "Initializing %s", self._app_display_name) Logger.log("d", "App Version %s", self._version) Logger.log("d", "Api Version %s", self._api_version) Logger.log("d", "Build type %s", self._build_type or "None") # For Ubuntu Unity this makes Qt use its own menu bar rather than pass it on to Unity. os.putenv("UBUNTU_MENUPROXY", "0") # Custom signal handling Signal._app = self Signal._signalQueue = self # Initialize Resources. Set the application name and version here because we can only know the actual info # after the __init__() has been called. Resources.ApplicationIdentifier = self._app_name Resources.ApplicationVersion = self._version app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable))) Resources.addSearchPath(os.path.join(app_root, "share", "uranium", "resources")) Resources.addSearchPath(os.path.join(os.path.dirname(sys.executable), "resources")) Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "uranium", "resources")) Resources.addSearchPath(os.path.join(self._app_install_dir, "Resources", "uranium", "resources")) Resources.addSearchPath(os.path.join(self._app_install_dir, "Resources", self._app_name, "resources")) if not hasattr(sys, "frozen"): Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")) i18nCatalog.setApplication(self) PluginRegistry.addType("backend", self.setBackend) PluginRegistry.addType("logger", Logger.addLogger) PluginRegistry.addType("extension", self.addExtension) PluginRegistry.addType("file_provider", self.addFileProvider) self._preferences = Preferences() self._preferences.addPreference("general/language", self._default_language) self._preferences.addPreference("general/visible_settings", "") self._preferences.addPreference("general/plugins_to_remove", "") self._preferences.addPreference("general/disabled_plugins", "") self._controller = Controller(self) self._output_device_manager = OutputDeviceManager() self._operation_stack = OperationStack(self._controller) self._plugin_registry = PluginRegistry(self) self._plugin_registry.addPluginLocation(os.path.join(app_root, "share", "uranium", "plugins")) self._plugin_registry.addPluginLocation(os.path.join(app_root, "share", "cura", "plugins")) self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "lib", "uranium")) self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "lib64", "uranium")) self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "lib32", "uranium")) self._plugin_registry.addPluginLocation(os.path.join(os.path.dirname(sys.executable), "plugins")) self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "Resources", "uranium", "plugins")) self._plugin_registry.addPluginLocation(os.path.join(self._app_install_dir, "Resources", self._app_name, "plugins")) # Locally installed plugins local_path = os.path.join(Resources.getStoragePath(Resources.Resources), "plugins") # Ensure the local plugins directory exists try: os.makedirs(local_path) except OSError: pass self._plugin_registry.addPluginLocation(local_path) if not hasattr(sys, "frozen"): self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins")) self._container_registry = self._container_registry_class(self) UM.Settings.InstanceContainer.setContainerRegistry(self._container_registry) UM.Settings.ContainerStack.setContainerRegistry(self._container_registry) self.showMessageSignal.connect(self.showMessage) self.hideMessageSignal.connect(self.hideMessage) def startSplashWindowPhase(self) -> None: pass def startPostSplashWindowPhase(self) -> None: pass # Indicates if we have just updated from an older application version. def hasJustUpdatedFromOldVersion(self) -> bool: return self._just_updated_from_old_version def run(self): """Run the main event loop. This method should be re-implemented by subclasses to start the main event loop. :exception NotImplementedError: """ self.addCommandLineOptions() self.parseCliOptions() self.initialize() self.startSplashWindowPhase() self.startPostSplashWindowPhase() def getContainerRegistry(self) -> ContainerRegistry: return self._container_registry def getApplicationLockFilename(self) -> str: """Get the lock filename""" return self._config_lock_filename applicationShuttingDown = Signal() """Emitted when the application window was closed and we need to shut down the application""" showMessageSignal = Signal() hideMessageSignal = Signal() globalContainerStackChanged = Signal() workspaceLoaded = Signal() def setGlobalContainerStack(self, stack: Optional["ContainerStack"]) -> None: if self._global_container_stack != stack: self._global_container_stack = stack self.globalContainerStackChanged.emit() def getGlobalContainerStack(self) -> Optional["ContainerStack"]: return self._global_container_stack def hideMessage(self, message: Message) -> None: raise NotImplementedError def showMessage(self, message: Message) -> None: raise NotImplementedError def showToastMessage(self, title: str, message: str) -> None: raise NotImplementedError def getVersion(self) -> str: """Get the version of the application""" return self._version def getBuildType(self) -> str: """Get the build type of the application""" return self._build_type def getIsDebugMode(self) -> bool: return self._is_debug_mode def getIsHeadLess(self) -> bool: return self._is_headless def getUseExternalBackend(self) -> bool: return self._use_external_backend visibleMessageAdded = Signal() def hideMessageById(self, message_id: int) -> None: """Hide message by ID (as provided by built-in id function)""" # If a user and the application tries to close same message dialog simultaneously, message_id could become an empty # string, and then the application will raise an error when trying to do "int(message_id)". # So we check the message_id here. if not message_id: return found_message = None with self._message_lock: for message in self._visible_messages: if id(message) == int(message_id): found_message = message if found_message is not None: self.hideMessageSignal.emit(found_message) visibleMessageRemoved = Signal() def getVisibleMessages(self) -> List[Message]: """Get list of all visible messages""" with self._message_lock: return self._visible_messages def _loadPlugins(self) -> None: """Function that needs to be overridden by child classes with a list of plugins it needs.""" pass def getApplicationName(self) -> str: """Get name of the application. :returns: app_name """ return self._app_name def getApplicationDisplayName(self) -> str: return self._app_display_name def getPreferences(self) -> Preferences: """Get the preferences. :return: preferences """ return self._preferences def savePreferences(self) -> None: if self._preferences_filename: self._preferences.writeToFile(self._preferences_filename) else: Logger.log("i", "Preferences filename not set. Unable to save file.") def getApplicationLanguage(self) -> str: """Get the currently used IETF language tag. The returned tag is during runtime used to translate strings. :returns: Language tag. """ language = os.getenv("URANIUM_LANGUAGE") if not language: language = self._preferences.getValue("general/language") if not language: language = os.getenv("LANGUAGE") if not language: language = self._default_language return language def getRequiredPlugins(self) -> List[str]: """Application has a list of plugins that it *must* have. If it does not have these, it cannot function. These plugins can not be disabled in any way. """ return self._required_plugins def setRequiredPlugins(self, plugin_names: List[str]) -> None: """Set the plugins that the application *must* have in order to function. :param plugin_names: List of strings with the names of the required plugins """ self._required_plugins = plugin_names def setBackend(self, backend: "Backend") -> None: """Set the backend of the application (the program that does the heavy lifting).""" self._backend = backend def getBackend(self) -> "Backend": """Get the backend of the application (the program that does the heavy lifting). :returns: Backend """ return self._backend def getPluginRegistry(self) -> PluginRegistry: """Get the PluginRegistry of this application. :returns: PluginRegistry """ return self._plugin_registry def getController(self) -> Controller: """Get the Controller of this application. :returns: Controller """ return self._controller def getOperationStack(self) -> OperationStack: return self._operation_stack def getOutputDeviceManager(self) -> OutputDeviceManager: return self._output_device_manager def getRenderer(self) -> Renderer: """Return an application-specific Renderer object. :exception NotImplementedError """ raise NotImplementedError("getRenderer must be implemented by subclasses.") def functionEvent(self, event: CallFunctionEvent) -> None: """Post a function event onto the event loop. This takes a CallFunctionEvent object and puts it into the actual event loop. :exception NotImplementedError """ raise NotImplementedError("functionEvent must be implemented by subclasses.") def callLater(self, func: Callable[..., Any], *args, **kwargs) -> None: """Call a function the next time the event loop runs. You can't get the result of this function directly. It won't block. :param func: The function to call. :param args: The positional arguments to pass to the function. :param kwargs: The keyword arguments to pass to the function. """ event = CallFunctionEvent(func, args, kwargs) self.functionEvent(event) def getMainThread(self) -> threading.Thread: """Get the application's main thread.""" return self._main_thread def addExtension(self, extension: "Extension") -> None: self._extensions.append(extension) def getExtensions(self) -> List["Extension"]: return self._extensions def addFileProvider(self, file_provider: "FileProvider") -> None: self._file_providers.append(file_provider) def getFileProviders(self) -> List["FileProvider"]: return self._file_providers # Returns the path to the folder of the app itself, e.g.: '/root/blah/programs/Cura'. @staticmethod def getAppFolderPrefix() -> str: if "python" in os.path.basename(sys.executable): executable = sys.argv[0] else: executable = sys.executable try: return os.path.dirname(os.path.realpath(executable)) except EnvironmentError: # Symlinks can't be dereferenced. return os.path.dirname(executable) # Returns the path to the folder the app is installed _in_, e.g.: '/root/blah/programs' @staticmethod def getInstallPrefix() -> str: return os.path.abspath(os.path.join(Application.getAppFolderPrefix(), "..")) __instance = None # type: Application @classmethod def getInstance(cls, *args, **kwargs) -> "Application": return cls.__instance
def test_connectSelf(): signal = Signal(type=Signal.Direct) signal.connect(signal) signal.emit() # If they are connected, this crashes with a max recursion depth error
class MockContainer(UM.Settings.ContainerInterface.ContainerInterface): ## Creates a mock container with a new unique ID. def __init__(self, container_id=None): self._id = uuid.uuid4().int if container_id == None else container_id self._metadata = {} self.items = {} ## Gets the unique ID of the container. # # \return A unique identifier for this container. def getId(self): return self._id ## Gives an arbitrary name. # # \return Some string. def getName(self): return "Fred" ## Get whether the container item is stored on a read only location in the filesystem. # # \return Always returns False def isReadOnly(self): return False ## Mock get path def getPath(self): return "/path/to/the/light/side" ## Mock set path def setPath(self, path): pass ## Returns the metadata dictionary. # # \return A dictionary containing metadata for this container stack. def getMetaData(self): return self._metadata ## Gets an entry from the metadata. # # \param entry The entry to get from the metadata. # \param default The default value in case the entry is missing. # \return The value belonging to the requested entry, or the default if no # such key exists. def getMetaDataEntry(self, entry, default=None): if entry in self._metadata: return self._metadata["entry"] return default ## Gets the value of a container item property. # # If the key doesn't exist, returns None. # # \param key The key of the item to get. def getProperty(self, key, property_name): if key in self.items: return self.items[key] return None propertyChanged = Signal() def hasProperty(self, key, property_name): return key in self.items ## Serialises this container. # # The serialisation of the mock needs to be kept simple, so it only # serialises the ID. This makes the tests succeed if the serialisation # creates different instances (which is desired). # # \return A static string representing a container. def serialize(self): return str(self._id) ## Deserialises a string to a container. # # The serialisation of the mock needs to be kept simple, so it only # deserialises the ID. This makes the tests succeed if the serialisation # creates different instances (which is desired). # # \param serialized A serialised mock container. def deserialize(self, serialized): self._id = int(serialized)
class CuraEngineBackend(Backend): def __init__(self): super().__init__() # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on. default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", "CuraEngine") if hasattr(sys, "frozen"): default_engine_location = os.path.join( os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine") if sys.platform == "win32": default_engine_location += ".exe" default_engine_location = os.path.abspath(default_engine_location) Preferences.getInstance().addPreference("backend/location", default_engine_location) self._scene = Application.getInstance().getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) self._onActiveViewChanged() self._stored_layer_data = None self._profile = None Application.getInstance().getMachineManager( ).activeProfileChanged.connect(self._onActiveProfileChanged) self._onActiveProfileChanged() self._change_timer = QTimer() self._change_timer.setInterval(500) self._change_timer.setSingleShot(True) self._change_timer.timeout.connect(self.slice) self._message_handlers[ Cura_pb2.SlicedObjectList] = self._onSlicedObjectListMessage self._message_handlers[Cura_pb2.Progress] = self._onProgressMessage self._message_handlers[Cura_pb2.GCodeLayer] = self._onGCodeLayerMessage self._message_handlers[ Cura_pb2.GCodePrefix] = self._onGCodePrefixMessage self._message_handlers[ Cura_pb2.ObjectPrintTime] = self._onObjectPrintTimeMessage self._slicing = False self._restart = False self._save_gcode = True self._save_polygons = True self._report_progress = True self._enabled = True self._message = None self.backendConnected.connect(self._onBackendConnected) Application.getInstance().getController().toolOperationStarted.connect( self._onToolOperationStarted) Application.getInstance().getController().toolOperationStopped.connect( self._onToolOperationStopped) Application.getInstance().getMachineManager( ).activeMachineInstanceChanged.connect(self._onInstanceChanged) ## Get the command that is used to call the engine. # This is usefull for debugging and used to actually start the engine # \return list of commands and args / parameters. def getEngineCommand(self): active_machine = Application.getInstance().getMachineManager( ).getActiveMachineInstance() if not active_machine: return None return [ Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", active_machine.getMachineDefinition().getPath(), "-vv" ] ## Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished. # \param time The amount of time the print will take. # \param material_amount The amount of material the print will use. printDurationMessage = Signal() ## Emitted when the slicing process starts. slicingStarted = Signal() ## Emitted whne the slicing process is aborted forcefully. slicingCancelled = Signal() ## Perform a slice of the scene with the given set of settings. # # \param kwargs Keyword arguments. # Valid values are: # - settings: The settings to use for the slice. The default is the active machine. # - save_gcode: True if the generated gcode should be saved, False if not. True by default. # - save_polygons: True if the generated polygon data should be saved, False if not. True by default. # - force_restart: True if the slicing process should be forcefully restarted if it is already slicing. # If False, this method will do nothing when already slicing. True by default. # - report_progress: True if the slicing progress should be reported, False if not. Default is True. def slice(self, **kwargs): if not self._enabled: return if self._slicing: if not kwargs.get("force_restart", True): return self._slicing = False self._restart = True if self._process is not None: Logger.log("d", "Killing engine process") try: self._process.terminate() except: # terminating a process that is already terminating causes an exception, silently ignore this. pass self.slicingCancelled.emit() return Logger.log("d", "Preparing to send slice data to engine.") object_groups = [] if self._profile.getSettingValue("print_sequence") == "one_at_a_time": for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] children = node.getAllChildren() children.append(node) for child_node in children: if type(child_node) is SceneNode and child_node.getMeshData( ) and child_node.getMeshData().getVertices() is not None: temp_list.append(child_node) object_groups.append(temp_list) else: temp_list = [] for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData( ) and node.getMeshData().getVertices() is not None: if not getattr(node, "_outside_buildarea", False): temp_list.append(node) if len(temp_list) == 0: self.processingProgress.emit(0.0) return object_groups.append(temp_list) #for node in DepthFirstIterator(self._scene.getRoot()): # if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None: # if not getattr(node, "_outside_buildarea", False): # objects.append(node) if len(object_groups) == 0: if self._message: self._message.hide() self._message = None return #No point in slicing an empty build plate if kwargs.get("profile", self._profile).hasErrorValue(): Logger.log('w', "Profile has error values. Aborting slicing") if self._message: self._message.hide() self._message = None self._message = Message( catalog.i18nc( "@info:status", "Unable to slice. Please check your setting values for errors." )) self._message.show() return #No slicing if we have error values since those are by definition illegal values. # Remove existing layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): Application.getInstance().getController().getScene( ).getRoot().removeChild(node) break Application.getInstance().getController().getScene().gcode_list = None self._slicing = True self.slicingStarted.emit() self._report_progress = kwargs.get("report_progress", True) if self._report_progress: self.processingProgress.emit(0.0) if not self._message: self._message = Message( catalog.i18nc("@info:status", "Slicing..."), 0, False, -1) self._message.show() else: self._message.setProgress(-1) self._sendSettings(kwargs.get("profile", self._profile)) self._scene.acquireLock() # Set the gcode as an empty list. This will be filled with strings by GCodeLayer messages. # This is done so the gcode can be fragmented in memory and does not need a continues memory space. # (AKA. This prevents MemoryErrors) self._save_gcode = kwargs.get("save_gcode", True) if self._save_gcode: setattr(self._scene, "gcode_list", []) self._save_polygons = kwargs.get("save_polygons", True) slice_message = Cura_pb2.Slice() for group in object_groups: group_message = slice_message.object_lists.add() for object in group: mesh_data = object.getMeshData().getTransformed( object.getWorldTransformation()) obj = group_message.objects.add() obj.id = id(object) verts = numpy.array(mesh_data.getVertices()) verts[:, [1, 2]] = verts[:, [2, 1]] verts[:, 1] *= -1 obj.vertices = verts.tostring() self._handlePerObjectSettings(object, obj) # Hack to add per-object settings also to the "MeshGroup" in CuraEngine # We really should come up with a better solution for this. self._handlePerObjectSettings(group[0], group_message) self._scene.releaseLock() Logger.log("d", "Sending data to engine for slicing.") self._socket.sendMessage(slice_message) def _onSceneChanged(self, source): if type(source) is not SceneNode: return if source is self._scene.getRoot(): return if source.getMeshData() is None: return if source.getMeshData().getVertices() is None: return self._onChanged() def _onActiveProfileChanged(self): if self._profile: self._profile.settingValueChanged.disconnect( self._onSettingChanged) self._profile = Application.getInstance().getMachineManager( ).getActiveProfile() if self._profile: self._profile.settingValueChanged.connect(self._onSettingChanged) self._onChanged() def _onSettingChanged(self, setting): self._onChanged() def _onSlicedObjectListMessage(self, message): if self._save_polygons: if self._layer_view_active: job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob( message) job.start() else: self._stored_layer_data = message def _onProgressMessage(self, message): if message.amount >= 0.99: self._slicing = False if self._message: self._message.setProgress(100) self._message.hide() self._message = None if self._message: self._message.setProgress(round(message.amount * 100)) if self._report_progress: self.processingProgress.emit(message.amount) def _onGCodeLayerMessage(self, message): if self._save_gcode: job = ProcessGCodeJob.ProcessGCodeLayerJob(message) job.start() def _onGCodePrefixMessage(self, message): if self._save_gcode: self._scene.gcode_list.insert( 0, message.data.decode("utf-8", "replace")) def _onObjectPrintTimeMessage(self, message): self.printDurationMessage.emit(message.time, message.material_amount) self.processingProgress.emit(1.0) def _createSocket(self): super()._createSocket() self._socket.registerMessageType(1, Cura_pb2.Slice) self._socket.registerMessageType(2, Cura_pb2.SlicedObjectList) self._socket.registerMessageType(3, Cura_pb2.Progress) self._socket.registerMessageType(4, Cura_pb2.GCodeLayer) self._socket.registerMessageType(5, Cura_pb2.ObjectPrintTime) self._socket.registerMessageType(6, Cura_pb2.SettingList) self._socket.registerMessageType(7, Cura_pb2.GCodePrefix) ## Manually triggers a reslice def forceSlice(self): self._change_timer.start() def _onChanged(self): if not self._profile: return self._change_timer.start() def _sendSettings(self, profile): msg = Cura_pb2.SettingList() for key, value in profile.getAllSettingValues( include_machine=True).items(): s = msg.settings.add() s.name = key s.value = str(value).encode("utf-8") self._socket.sendMessage(msg) def _onBackendConnected(self): if self._restart: self._onChanged() self._restart = False def _onToolOperationStarted(self, tool): self._enabled = False # Do not reslice when a tool is doing it's 'thing' def _onToolOperationStopped(self, tool): self._enabled = True # Tool stop, start listening for changes again. self._onChanged() def _onActiveViewChanged(self): if Application.getInstance().getController().getActiveView(): view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": self._layer_view_active = True if self._stored_layer_data: job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob( self._stored_layer_data) job.start() self._stored_layer_data = None else: self._layer_view_active = False def _handlePerObjectSettings(self, node, message): profile = node.callDecoration("getProfile") if profile: for key, value in profile.getChangedSettingValues().items(): setting = message.settings.add() setting.name = key setting.value = str(value).encode() object_settings = node.callDecoration("getAllSettingValues") if not object_settings: return for key, value in object_settings.items(): setting = message.settings.add() setting.name = key setting.value = str(value).encode() def _onInstanceChanged(self): self._slicing = False self._restart = True if self._process is not None: Logger.log("d", "Killing engine process") try: self._process.terminate() except: # terminating a process that is already terminating causes an exception, silently ignore this. pass self.slicingCancelled.emit()
class MainWindow(QQuickWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self._background_color = QColor(204, 204, 204, 255) self.setClearBeforeRendering(False) self.beforeRendering.connect(self._render, type=Qt.DirectConnection) self._mouse_device = QtMouseDevice(self) self._mouse_device.setPluginId("qt_mouse") self._key_device = QtKeyDevice() self._key_device.setPluginId("qt_key") self._previous_focus = None # type: Optional["QQuickItem"] self._app = QCoreApplication.instance() # Remove previously added input devices (if any). This can happen if the window was re-loaded. self._app.getController().removeInputDevice("qt_mouse") self._app.getController().removeInputDevice("qt_key") self._app.getController().addInputDevice(self._mouse_device) self._app.getController().addInputDevice(self._key_device) self._app.getController().getScene().sceneChanged.connect( self._onSceneChanged) self._preferences = Preferences.getInstance() self._preferences.addPreference("general/window_width", 1280) self._preferences.addPreference("general/window_height", 720) self._preferences.addPreference("general/window_left", 50) self._preferences.addPreference("general/window_top", 50) self._preferences.addPreference("general/window_state", Qt.WindowNoState) # Restore window geometry self.setWidth(int(self._preferences.getValue("general/window_width"))) self.setHeight(int( self._preferences.getValue("general/window_height"))) self.setPosition( int(self._preferences.getValue("general/window_left")), int(self._preferences.getValue("general/window_top"))) # Make sure restored geometry is not outside the currently available screens screen_found = False for s in range(0, self._app.desktop().screenCount()): if self.geometry().intersects( self._app.desktop().availableGeometry(s)): screen_found = True break if not screen_found: self.setPosition(50, 50) self.setWindowState( int(self._preferences.getValue("general/window_state"))) self._mouse_x = 0 self._mouse_y = 0 self._mouse_pressed = False self._viewport_rect = QRectF(0, 0, 1.0, 1.0) Application.getInstance().setMainWindow(self) self._fullscreen = False @pyqtSlot() def toggleFullscreen(self): if self._fullscreen: self.setVisibility( QQuickWindow.Windowed) # Switch back to windowed else: self.setVisibility(QQuickWindow.FullScreen) # Go to fullscreen self._fullscreen = not self._fullscreen def getBackgroundColor(self): return self._background_color def setBackgroundColor(self, color): self._background_color = color self._app.getRenderer().setBackgroundColor(color) backgroundColor = pyqtProperty(QColor, fget=getBackgroundColor, fset=setBackgroundColor) mousePositionChanged = pyqtSignal() @pyqtProperty(int, notify=mousePositionChanged) def mouseX(self): return self._mouse_x @pyqtProperty(int, notify=mousePositionChanged) def mouseY(self): return self._mouse_y def setViewportRect(self, rect): if rect != self._viewport_rect: self._viewport_rect = rect self._updateViewportGeometry( self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) self.viewportRectChanged.emit() viewportRectChanged = pyqtSignal() @pyqtProperty(QRectF, fset=setViewportRect, notify=viewportRectChanged) def viewportRect(self): return self._viewport_rect # Warning! Never reimplemented this as a QExposeEvent can cause a deadlock with QSGThreadedRender due to both trying # to claim the Python GIL. # def event(self, event): def mousePressEvent(self, event): super().mousePressEvent(event) if event.isAccepted(): return if self.activeFocusItem( ) is not None and self.activeFocusItem() != self._previous_focus: self.activeFocusItem().setFocus(False) self._previous_focus = self.activeFocusItem() self._mouse_device.handleEvent(event) self._mouse_pressed = True def mouseMoveEvent(self, event): self._mouse_x = event.x() self._mouse_y = event.y() if self._mouse_pressed and self._app.getController( ).isModelRenderingEnabled(): self.mousePositionChanged.emit() super().mouseMoveEvent(event) if event.isAccepted(): return self._mouse_device.handleEvent(event) def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) if event.isAccepted(): return self._mouse_device.handleEvent(event) self._mouse_pressed = False def keyPressEvent(self, event): super().keyPressEvent(event) if event.isAccepted(): return self._key_device.handleEvent(event) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) if event.isAccepted(): return self._key_device.handleEvent(event) def wheelEvent(self, event): super().wheelEvent(event) if event.isAccepted(): return self._mouse_device.handleEvent(event) def moveEvent(self, event): QMetaObject.invokeMethod(self, "_onWindowGeometryChanged", Qt.QueuedConnection) def resizeEvent(self, event): super().resizeEvent(event) win_w = event.size().width() * self.devicePixelRatio() win_h = event.size().height() * self.devicePixelRatio() self._updateViewportGeometry(win_w, win_h) QMetaObject.invokeMethod(self, "_onWindowGeometryChanged", Qt.QueuedConnection) def hideEvent(self, event): if Application.getInstance().getMainWindow() == self: Application.getInstance().windowClosed() renderCompleted = Signal(type=Signal.Queued) def _render(self): renderer = self._app.getRenderer() view = self._app.getController().getActiveView() renderer.beginRendering() view.beginRendering() renderer.render() view.endRendering() renderer.endRendering() self.renderCompleted.emit() def _onSceneChanged(self, object): self.update() @pyqtSlot() def _onWindowGeometryChanged(self): if self.windowState() == Qt.WindowNoState: self._preferences.setValue("general/window_width", self.width()) self._preferences.setValue("general/window_height", self.height()) self._preferences.setValue("general/window_left", self.x()) self._preferences.setValue("general/window_top", self.y()) self._preferences.setValue("general/window_state", Qt.WindowNoState) elif self.windowState() == Qt.WindowMaximized: self._preferences.setValue("general/window_state", Qt.WindowMaximized) def _updateViewportGeometry(self, width: int, height: int): view_width = width * self._viewport_rect.width() view_height = height * self._viewport_rect.height() for camera in self._app.getController().getScene().getAllCameras(): camera.setWindowSize(width, height) if camera.getAutoAdjustViewPort(): camera.setViewportSize(view_width, view_height) projection_matrix = Matrix() if camera.isPerspective(): if view_width is not 0: projection_matrix.setPerspective( 30, view_width / view_height, 1, 500) else: projection_matrix.setOrtho(-view_width / 2, view_width / 2, -view_height / 2, view_height / 2, -500, 500) camera.setProjectionMatrix(projection_matrix) self._app.getRenderer().setViewportSize(view_width, view_height) self._app.getRenderer().setWindowSize(width, height)
class SceneNode: """A scene node object. These objects can hold a mesh and multiple children. Each node has a transformation matrix that maps it it's parents space to the local space (it's inverse maps local space to parent). SceneNodes can be "Decorated" by adding SceneNodeDecorator objects. These decorators can add functionality to scene nodes. :sa SceneNodeDecorator :todo Add unit testing """ class TransformSpace: Local = 1 #type: int Parent = 2 #type: int World = 3 #type: int def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", node_id: str = "") -> None: """Construct a scene node. :param parent: The parent of this node (if any). Only a root node should have None as a parent. :param visible: Is the SceneNode (and thus, all its children) visible? :param name: Name of the SceneNode. """ super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: Optional[MeshData] self.metadata = {} # type: Dict[str, Any] # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # This is used for rendering. Since we don't want to recompute it every time, we cache it in the node self._cached_normal_matrix = Matrix() # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBoundingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = visible # type: bool self._name = name # type: str self._id = node_id # type: str self._decorators = [] # type: List[SceneNodeDecorator] # Store custom settings to be compatible with Savitar SceneNode self._settings = {} # type: Dict[str, Any] ## Signals self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo: Dict[int, object]) -> "SceneNode": copy = self.__class__() copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) copy._visible = cast(bool, deepcopy(self._visible, memo)) copy._selectable = cast(bool, deepcopy(self._selectable, memo)) copy._name = cast(str, deepcopy(self._name, memo)) for decorator in self._decorators: copy.addDecorator( cast(SceneNodeDecorator, deepcopy(decorator, memo))) for child in self._children: copy.addChild(cast(SceneNode, deepcopy(child, memo))) self.calculateBoundingBoxMesh() return copy def setCenterPosition(self, center: Vector) -> None: """Set the center position of this node. This is used to modify it's mesh data (and it's children) in such a way that they are centered. In most cases this means that we use the center of mass as center (which most objects don't use) """ if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m).set( center_position=center) for child in self._children: child.setCenterPosition(center) def getParent(self) -> Optional["SceneNode"]: """Get the parent of this node. If the node has no parent, it is the root node. :returns: SceneNode if it has a parent and None if it's the root node. """ return self._parent def getMirror(self) -> Vector: return self._mirror def setMirror(self, vector) -> None: self._mirror = vector def getBoundingBoxMesh(self) -> Optional[MeshData]: """Get the MeshData of the bounding box :returns: :type{MeshData} Bounding box mesh. """ if self._bounding_box_mesh is None: self.calculateBoundingBoxMesh() return self._bounding_box_mesh def calculateBoundingBoxMesh(self) -> None: """(re)Calculate the bounding box mesh.""" aabb = self.getBoundingBox() if aabb: bounding_box_mesh = MeshBuilder() rtf = aabb.maximum lbb = aabb.minimum bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh = bounding_box_mesh.build() def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool: """Return if the provided bbox collides with the bbox of this SceneNode""" bbox = self.getBoundingBox() if bbox is not None: if check_bbox.intersectsBox( bbox ) != AxisAlignedBox.IntersectionResult.FullIntersection: return True return False def _onParentChanged(self, node: Optional["SceneNode"]) -> None: """Handler for the ParentChanged signal :param node: Node from which this event was triggered. """ for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() """Signal for when a :type{SceneNodeDecorator} is added / removed.""" def addDecorator(self, decorator: SceneNodeDecorator) -> None: """Add a SceneNodeDecorator to this SceneNode. :param decorator: The decorator to add. """ if type(decorator) in [type(dec) for dec in self._decorators]: Logger.log( "w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator)) return try: decorator.setNode(self) except AttributeError: Logger.logException("e", "Unable to add decorator.") return self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self) -> List[SceneNodeDecorator]: """Get all SceneNodeDecorators that decorate this SceneNode. :return: list of all SceneNodeDecorators. """ return self._decorators def getDecorator(self, dec_type: type) -> Optional[SceneNodeDecorator]: """Get SceneNodeDecorators by type. :param dec_type: type of decorator to return. """ for decorator in self._decorators: if type(decorator) == dec_type: return decorator return None def removeDecorators(self): """Remove all decorators""" for decorator in self._decorators: decorator.clear() self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type: type) -> None: """Remove decorator by type. :param dec_type: type of the decorator to remove. """ for decorator in self._decorators: if type(decorator) == dec_type: decorator.clear() self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function: str, *args, **kwargs) -> Any: """Call a decoration of this SceneNode. SceneNodeDecorators add Decorations, which are callable functions. :param function: The function to be called. :param *args :param **kwargs """ for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.logException("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function: str) -> bool: """Does this SceneNode have a certain Decoration (as defined by a Decorator) :param :type{string} function the function to check for. """ for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self) -> str: return self._name def setName(self, name: str) -> None: self._name = name def getId(self) -> str: return self._id def setId(self, node_id: str) -> None: self._id = node_id def getDepth(self) -> int: """How many nodes is this node removed from the root? :return: Steps from root (0 means it -is- the root). """ if self._parent is None: return 0 return self._parent.getDepth() + 1 def setParent(self, scene_node: Optional["SceneNode"]) -> None: """:brief Set the parent of this object :param scene_node: SceneNode that is the parent of this object. """ if self._parent: self._parent.removeChild(self) if scene_node: scene_node.addChild(self) parentChanged = Signal() """Emitted whenever the parent changes.""" def isVisible(self) -> bool: """Get the visibility of this node. The parents visibility overrides the visibility. TODO: Let renderer actually use the visibility to decide whether to render or not. """ if self._parent is not None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible: bool) -> None: """Set the visibility of this SceneNode.""" self._visible = visible def getMeshData(self) -> Optional[MeshData]: """Get the (original) mesh data from the scene node/object. :returns: MeshData """ return self._mesh_data def getMeshDataTransformed(self) -> Optional[MeshData]: """Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. If this node is a group, it will recursively concatenate all child nodes/objects. :returns: MeshData """ return MeshData(vertices=self.getMeshDataTransformedVertices(), normals=self.getMeshDataTransformedNormals()) def getMeshDataTransformedVertices(self) -> Optional[numpy.ndarray]: """Get the transformed vertices from this scene node/object, based on the transformation of scene nodes wrt root. If this node is a group, it will recursively concatenate all child nodes/objects. :return: numpy.ndarray """ transformed_vertices = None if self.callDecoration("isGroup"): for child in self._children: tv = child.getMeshDataTransformedVertices() if transformed_vertices is None: transformed_vertices = tv else: transformed_vertices = numpy.concatenate( (transformed_vertices, tv), axis=0) else: if self._mesh_data: transformed_vertices = self._mesh_data.getTransformed( self.getWorldTransformation(copy=False)).getVertices() return transformed_vertices def getMeshDataTransformedNormals(self) -> Optional[numpy.ndarray]: """Get the transformed normals from this scene node/object, based on the transformation of scene nodes wrt root. If this node is a group, it will recursively concatenate all child nodes/objects. :return: numpy.ndarray """ transformed_normals = None if self.callDecoration("isGroup"): for child in self._children: tv = child.getMeshDataTransformedNormals() if transformed_normals is None: transformed_normals = tv else: transformed_normals = numpy.concatenate( (transformed_normals, tv), axis=0) else: if self._mesh_data: transformed_normals = self._mesh_data.getTransformed( self.getWorldTransformation(copy=False)).getNormals() return transformed_normals def setMeshData(self, mesh_data: Optional[MeshData]) -> None: """Set the mesh of this node/object :param mesh_data: MeshData object """ self._mesh_data = mesh_data self._resetAABB() self.meshDataChanged.emit(self) meshDataChanged = Signal() """Emitted whenever the attached mesh data object changes.""" def _onMeshDataChanged(self) -> None: self.meshDataChanged.emit(self) def addChild(self, scene_node: "SceneNode") -> None: """Add a child to this node and set it's parent as this node. :params scene_node SceneNode to add. """ if scene_node in self._children: return scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) def removeChild(self, child: "SceneNode") -> None: """remove a single child :param child: Scene node that needs to be removed. """ if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) try: self._children.remove(child) except ValueError: # Could happen that the child was removed asynchronously by a different thread. Don't crash by removing it twice. pass # But still update the AABB and such. child._parent = None child._transformChanged() child.parentChanged.emit(self) self._resetAABB() self.childrenChanged.emit(self) def removeAllChildren(self) -> None: """Removes all children and its children's children.""" for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) def getChildren(self) -> List["SceneNode"]: """Get the list of direct children :returns: List of children """ return self._children def hasChildren(self) -> bool: return True if self._children else False def getAllChildren(self) -> List["SceneNode"]: """Get list of all children (including it's children children children etc.) :returns: list ALl children in this 'tree' """ children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children childrenChanged = Signal() """Emitted whenever the list of children of this object or any child object changes. :param object: The object that triggered the change. """ def _updateCachedNormalMatrix(self) -> None: self._cached_normal_matrix = Matrix( self.getWorldTransformation(copy=False).getData()) self._cached_normal_matrix.setRow(3, [0, 0, 0, 1]) self._cached_normal_matrix.setColumn(3, [0, 0, 0, 1]) self._cached_normal_matrix.pseudoinvert() self._cached_normal_matrix.transpose() def getCachedNormalMatrix(self) -> Matrix: if self._cached_normal_matrix is None: self._updateCachedNormalMatrix() return self._cached_normal_matrix def getWorldTransformation(self, copy=True) -> Matrix: """Computes and returns the transformation from world to local space. :returns: 4x4 transformation matrix """ if self._world_transformation is None: self._updateWorldTransformation() if copy: return self._world_transformation.copy() return self._world_transformation def getLocalTransformation(self, copy=True) -> Matrix: """Returns the local transformation with respect to its parent. (from parent to local) :returns transformation 4x4 (homogeneous) matrix """ if self._transformation is None: self._updateLocalTransformation() if copy: return self._transformation.copy() return self._transformation def setTransformation(self, transformation: Matrix): self._transformation = transformation.copy( ) # Make a copy to ensure we never change the given transformation self._transformChanged() def getOrientation(self) -> Quaternion: """Get the local orientation value.""" return deepcopy(self._orientation) def getWorldOrientation(self) -> Quaternion: return deepcopy(self._derived_orientation) def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local) -> None: """Rotate the scene object (and thus its children) by given amount :param rotation: :type{Quaternion} A quaternion indicating the amount of rotation. :param transform_space: The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. """ if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local) -> None: """Set the local orientation of this scene node. :param orientation: :type{Quaternion} The new orientation of this scene node. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or orientation == self._orientation: return if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).invert() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix = Matrix() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged() def getScale(self) -> Vector: """Get the local scaling value.""" return self._scale def getWorldScale(self) -> Vector: return self._derived_scale def scale(self, scale: Vector, transform_space: int = TransformSpace.Local) -> None: """Scale the scene object (and thus its children) by given amount :param scale: :type{Vector} A Vector with three scale values :param transform_space: The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. """ if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() def setScale(self, scale: Vector, transform_space: int = TransformSpace.Local) -> None: """Set the local scale value. :param scale: :type{Vector} The new scale value of the scene node. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) def getPosition(self) -> Vector: """Get the local position.""" return self._position def getWorldPosition(self) -> Vector: """Get the position of this scene node relative to the world.""" return self._derived_position def translate(self, translation: Vector, transform_space: int = TransformSpace.Local) -> None: """Translate the scene object (and thus its children) by given amount. :param translation: :type{Vector} The amount to translate by. :param transform_space: The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. """ if not self._enabled: return translation_matrix = Matrix() translation_matrix.setByTranslation(translation) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.World: world_transformation = self._world_transformation.copy() self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(world_transformation) self._transformChanged() def setPosition(self, position: Vector, transform_space: int = TransformSpace.Local) -> None: """Set the local position value. :param position: The new position value of the SceneNode. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._derived_position, SceneNode.TransformSpace.World) transformationChanged = Signal() """Signal. Emitted whenever the transformation of this object or any child object changes. :param object: The object that caused the change. """ def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y) -> None: """Rotate this scene node in such a way that it is looking at target. :param target: :type{Vector} The target to look at. :param up: :type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). """ if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) self.setOrientation(Quaternion.fromMatrix(m)) def render(self, renderer) -> bool: """Can be overridden by child nodes if they need to perform special rendering. If you need to handle rendering in a special way, for example for tool handles, you can override this method and render the node. Return True to prevent the view from rendering any attached mesh data. :param renderer: The renderer object to use for rendering. :return: False if the view should render this node, True if we handle our own rendering. """ return False def isEnabled(self) -> bool: """Get whether this SceneNode is enabled, that is, it can be modified in any way.""" if self._parent is not None and self._enabled: return self._parent.isEnabled() else: return self._enabled def setEnabled(self, enable: bool) -> None: """Set whether this SceneNode is enabled. :param enable: True if this object should be enabled, False if not. :sa isEnabled """ self._enabled = enable def isSelectable(self) -> bool: """Get whether this SceneNode can be selected. :note This will return false if isEnabled() returns false. """ return self._enabled and self._selectable def setSelectable(self, select: bool) -> None: """Set whether this SceneNode can be selected. :param select: True if this SceneNode should be selectable, False if not. """ self._selectable = select def getBoundingBox(self) -> Optional[AxisAlignedBox]: """Get the bounding box of this node and its children.""" if not self._calculate_aabb: return None if self._aabb is None: self._calculateAABB() return self._aabb def setCalculateBoundingBox(self, calculate: bool) -> None: """Set whether or not to calculate the bounding box for this node. :param calculate: True if the bounding box should be calculated, False if not. """ self._calculate_aabb = calculate boundingBoxChanged = Signal() def getShear(self) -> Vector: return self._shear def getSetting(self, key: str, default_value: str = "") -> str: return self._settings.get(key, default_value) def setSetting(self, key: str, value: str) -> None: self._settings[key] = value def invertNormals(self) -> None: for child in self._children: child.invertNormals() if self._mesh_data: self._mesh_data.invertNormals() def _transformChanged(self) -> None: self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateLocalTransformation(self) -> None: self._position, euler_angle_matrix, self._scale, self._shear = self._transformation.decompose( ) self._orientation.setByMatrix(euler_angle_matrix) def _updateWorldTransformation(self) -> None: if self._parent: self._world_transformation = self._parent.getWorldTransformation( ).multiply(self._transformation) else: self._world_transformation = self._transformation self._derived_position, world_euler_angle_matrix, self._derived_scale, world_shear = self._world_transformation.decompose( ) self._derived_orientation.setByMatrix(world_euler_angle_matrix) def _updateTransformation(self) -> None: self._updateLocalTransformation() self._updateWorldTransformation() self._updateCachedNormalMatrix() def _resetAABB(self) -> None: if not self._calculate_aabb: return self._aabb = None self._bounding_box_mesh = None if self._parent: self._parent._resetAABB() self.boundingBoxChanged.emit() def _calculateAABB(self) -> None: if self._mesh_data: aabb = self._mesh_data.getExtents( self.getWorldTransformation(copy=False)) else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) position = self.getWorldPosition() aabb = AxisAlignedBox(minimum=position, maximum=position) for child in self._children: if aabb is None: aabb = child.getBoundingBox() else: aabb = aabb + child.getBoundingBox() self._aabb = aabb def __str__(self) -> str: """String output for debugging.""" name = self._name if self._name != "" else hex(id(self)) return "<" + self.__class__.__qualname__ + " object: '" + name + "'>"
class CloudPackageChecker(QObject): SYNC_SERVICE_NAME = "CloudPackageChecker" def __init__(self, application: CuraApplication) -> None: super().__init__() self.discrepancies = Signal() # Emits SubscribedPackagesModel self._application = application # type: CuraApplication self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) self._model = SubscribedPackagesModel() self._message = None # type: Optional[Message] self._application.initializationFinished.connect( self._onAppInitialized) self._i18n_catalog = i18nCatalog("cura") self._sdk_version = ApplicationMetadata.CuraSDKVersion self._last_notified_packages = set() # type: Set[str] """Packages for which a notification has been shown. No need to bother the user twice fo equal content""" # This is a plugin, so most of the components required are not ready when # this is initialized. Therefore, we wait until the application is ready. def _onAppInitialized(self) -> None: self._package_manager = self._application.getPackageManager() # initial check self._getPackagesIfLoggedIn() self._application.getCuraAPI().account.loginStateChanged.connect( self._onLoginStateChanged) self._application.getCuraAPI().account.syncRequested.connect( self._getPackagesIfLoggedIn) def _onLoginStateChanged(self) -> None: # reset session self._last_notified_packages = set() self._getPackagesIfLoggedIn() def _getPackagesIfLoggedIn(self) -> None: if self._application.getCuraAPI().account.isLoggedIn: self._getUserSubscribedPackages() else: self._hideSyncMessage() def _getUserSubscribedPackages(self) -> None: self._application.getCuraAPI().account.setSyncState( self.SYNC_SERVICE_NAME, SyncState.SYNCING) url = CloudApiModel.api_url_user_packages self._application.getHttpRequestManager().get( url, callback=self._onUserPackagesRequestFinished, error_callback=self._onUserPackagesRequestFinished, timeout=10, scope=self._scope) def _onUserPackagesRequestFinished( self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: if error is not None or reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) != 200: Logger.log( "w", "Requesting user packages failed, response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) self._application.getCuraAPI().account.setSyncState( self.SYNC_SERVICE_NAME, SyncState.ERROR) return try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) # Check for errors: if "errors" in json_data: for error in json_data["errors"]: Logger.log("e", "%s", error["title"]) self._application.getCuraAPI().account.setSyncState( self.SYNC_SERVICE_NAME, SyncState.ERROR) return self._handleCompatibilityData(json_data["data"]) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON for user subscribed packages from the Web Marketplace" ) self._application.getCuraAPI().account.setSyncState( self.SYNC_SERVICE_NAME, SyncState.SUCCESS) def _handleCompatibilityData( self, subscribed_packages_payload: List[Dict[str, Any]]) -> None: user_subscribed_packages = { plugin["package_id"] for plugin in subscribed_packages_payload } user_installed_packages = self._package_manager.getAllInstalledPackageIDs( ) # We need to re-evaluate the dismissed packages # (i.e. some package might got updated to the correct SDK version in the meantime, # hence remove them from the Dismissed Incompatible list) self._package_manager.reEvaluateDismissedPackages( subscribed_packages_payload, self._sdk_version) user_dismissed_packages = self._package_manager.getDismissedPackages() if user_dismissed_packages: user_installed_packages += user_dismissed_packages # We check if there are packages installed in Web Marketplace but not in Cura marketplace package_discrepancy = list( user_subscribed_packages.difference(user_installed_packages)) if user_subscribed_packages != self._last_notified_packages: # scenario: # 1. user subscribes to a package # 2. dismisses the license/unsubscribes # 3. subscribes to the same package again # in this scenario we want to notify the user again. To capture that there was a change during # step 2, we clear the last_notified after step 2. This way, the user will be notified after # step 3 even though the list of packages for step 1 and 3 are equal self._last_notified_packages = set() if package_discrepancy: account = self._application.getCuraAPI().account account.setUpdatePackagesAction( lambda: self._onSyncButtonClicked(None, None)) if user_subscribed_packages == self._last_notified_packages: # already notified user about these return Logger.log( "d", "Discrepancy found between Cloud subscribed packages and Cura installed packages" ) self._model.addDiscrepancies(package_discrepancy) self._model.initialize(self._package_manager, subscribed_packages_payload) self._showSyncMessage() self._last_notified_packages = user_subscribed_packages def _showSyncMessage(self) -> None: """Show the message if it is not already shown""" if self._message is not None: self._message.show() return sync_message = Message( self._i18n_catalog.i18nc( "@info:generic", "Do you want to sync material and software packages with your account?" ), title=self._i18n_catalog.i18nc( "@info:title", "Changes detected from your Ultimaker account", ), lifetime=0) sync_message.addAction( "sync", name=self._i18n_catalog.i18nc("@action:button", "Sync"), icon="", description= "Sync your Cloud subscribed packages to your local environment.", button_align=Message.ActionButtonAlignment.ALIGN_RIGHT) sync_message.actionTriggered.connect(self._onSyncButtonClicked) sync_message.show() self._message = sync_message def _hideSyncMessage(self) -> None: """Hide the message if it is showing""" if self._message is not None: self._message.hide() self._message = None def _onSyncButtonClicked(self, sync_message: Optional[Message], sync_message_action: Optional[str]) -> None: if sync_message is not None: sync_message.hide() self._hideSyncMessage( ) # Should be the same message, but also sets _message to None self.discrepancies.emit(self._model)
class Tool(PluginObject, SignalEmitter): def __init__(self): super().__init__() # Call super to make multiple inheritence work. self._controller = UM.Application.Application.getInstance( ).getController() # Circular dependency blah self._handle = None self._locked_axis = None self._drag_plane = None self._drag_start = None self._exposed_properties = [] ## Should be emitted whenever a longer running operation is started, like a drag to scale an object. # # \param tool The tool that started the operation. operationStarted = Signal() ## Should be emitted whenever a longer running operation is stopped. # # \param tool The tool that stopped the operation. operationStopped = Signal() propertyChanged = Signal() def getExposedProperties(self): return self._exposed_properties def setExposedProperties(self, *args): self._exposed_properties = args ## Handle an event. # \param event \type{Event} The event to handle. # \sa Event def event(self, event): if event.type == Event.ToolActivateEvent: if Selection.hasSelection() and self._handle: self._handle.setParent( self.getController().getScene().getRoot()) if event.type == Event.MouseMoveEvent and self._handle: if self._locked_axis: return id = self._renderer.getIdAtCoordinate(event.x, event.y) if self._handle.isAxis(id): self._handle.setActiveAxis(id) else: self._handle.setActiveAxis(None) if event.type == Event.ToolDeactivateEvent and self._handle: self._handle.setParent(None) return False ## Convenience function def getController(self): return self._controller ## Get the associated handle # \return \type{ToolHandle} def getHandle(self): return self._handle ## set the associated handle # \param \type{ToolHandle} def setHandle(self, handle): self._handle = handle def getLockedAxis(self): return self._locked_axis def setLockedAxis(self, axis): self._locked_axis = axis if self._handle: self._handle.setActiveAxis(axis) def getDragPlane(self): return self._drag_plane def setDragPlane(self, plane): self._drag_plane = plane def getDragStart(self): return self._drag_start def setDragStart(self, x, y): self._drag_start = self.getDragPosition(x, y) def getDragPosition(self, x, y): if not self._drag_plane: return None ray = self._controller.getScene().getActiveCamera().getRay(x, y) target = self._drag_plane.intersectsRay(ray) if target: return ray.getPointAlongRay(target) return None def getDragVector(self, x, y): if not self._drag_plane: return None if not self._drag_start: return None drag_end = self.getDragPosition(x, y) if drag_end: return drag_end - self._drag_start return None
class SettingOverrideDecorator(SceneNodeDecorator): ## Event indicating that the user selected a different extruder. activeExtruderChanged = Signal() ## Non-printing meshes # # If these settings are True for any mesh, the mesh does not need a convex hull, # and is sent to the slicer regardless of whether it fits inside the build volume. # Note that Support Mesh is not in here because it actually generates # g-code in the volume of the mesh. _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} def __init__(self): super().__init__() self._stack = PerObjectContainerStack(stack_id = "per_object_stack_" + str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer")) self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() self._is_non_printing_mesh = False self._error_check_timer = QTimer() self._error_check_timer.setInterval(250) self._error_check_timer.setSingleShot(True) self._error_check_timer.timeout.connect(self._checkStackForErrors) self._stack.propertyChanged.connect(self._onSettingChanged) Application.getInstance().getContainerRegistry().addContainer(self._stack) Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack) self.activeExtruderChanged.connect(self._updateNextStack) self._updateNextStack() def __deepcopy__(self, memo): ## Create a fresh decorator object deep_copy = SettingOverrideDecorator() ## Copy the instance instance_container = copy.deepcopy(self._stack.getContainer(0), memo) ## Set the copied instance as the first (and only) instance container of the stack. deep_copy._stack.replaceContainer(0, instance_container) # Properly set the right extruder on the copy deep_copy.setActiveExtruder(self._extruder_stack) # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # has not been updated yet. deep_copy._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) return deep_copy ## Gets the currently active extruder to print this object with. # # \return An extruder's container stack. def getActiveExtruder(self): return self._extruder_stack ## Gets the signal that emits if the active extruder changed. # # This can then be accessed via a decorator. def getActiveExtruderChangedSignal(self): return self.activeExtruderChanged ## Gets the currently active extruders position # # \return An extruder's position, or None if no position info is available. def getActiveExtruderPosition(self): containers = ContainerRegistry.getInstance().findContainers(id = self.getActiveExtruder()) if containers: container_stack = containers[0] return container_stack.getMetaDataEntry("position", default=None) def isNonPrintingMesh(self): return self._is_non_printing_mesh def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function # Trigger slice/need slicing if the value has changed. if property_name == "value": self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) if not self._is_non_printing_mesh: # self._error_check_timer.start() self._checkStackForErrors() Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() def _checkStackForErrors(self): hasErrors = False; for key in self._stack.getAllKeys(): validation_state = self._stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): Logger.log("w", "Setting Per Object %s is not valid.", key) hasErrors = True break Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors) ## Makes sure that the stack upon which the container stack is placed is # kept up to date. def _updateNextStack(self): if self._extruder_stack: extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack) if extruder_stack: if self._stack.getNextStack(): old_extruder_stack_id = self._stack.getNextStack().getId() else: old_extruder_stack_id = "" self._stack.setNextStack(extruder_stack[0]) # Trigger slice/need slicing if the extruder changed. if self._stack.getNextStack().getId() != old_extruder_stack_id: Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() else: Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack) else: self._stack.setNextStack(Application.getInstance().getGlobalContainerStack()) ## Changes the extruder with which to print this node. # # \param extruder_stack_id The new extruder stack to print with. def setActiveExtruder(self, extruder_stack_id): self._extruder_stack = extruder_stack_id self._updateNextStack() ExtruderManager.getInstance().resetSelectedObjectExtruders() self.activeExtruderChanged.emit() def getStack(self): return self._stack
class DefinitionContainer(ContainerInterface.ContainerInterface, PluginObject): Version = 2 ## Constructor # # \param container_id A unique, machine readable/writable ID for this container. def __init__(self, container_id, i18n_catalog=None, *args, **kwargs): super().__init__(*args, **kwargs) self._id = str(container_id) self._name = container_id self._metadata = {} self._definitions = [] self._inherited_files = [] self._i18n_catalog = i18n_catalog self._definition_cache = {} ## Reimplement __setattr__ so we can make sure the definition remains unchanged after creation. def __setattr__(self, name, value): super().__setattr__(name, value) #raise NotImplementedError() ## \copydoc ContainerInterface::getId # # Reimplemented from ContainerInterface def getId(self): return self._id id = property(getId) ## \copydoc ContainerInterface::getName # # Reimplemented from ContainerInterface def getName(self): return self._name name = property(getName) ## \copydoc ContainerInterface::isReadOnly # # Reimplemented from ContainerInterface def isReadOnly(self): return True def setReadOnly(self, read_only): pass ## \copydoc ContainerInterface::getMetaData # # Reimplemented from ContainerInterface def getMetaData(self): return self._metadata metaData = property(getMetaData) @property def definitions(self): return self._definitions ## Gets all ancestors of this definition container. # # This returns the definition in the "inherits" property of this # container, and the definition in its "inherits" property, and so on. The # ancestors are returned in order from parent to # grand-grand-grand-...-grandparent, normally ending in a "root" # container. # # \return A list of ancestors, in order from near ancestor to the root. def getInheritedFiles(self): return self._inherited_files ## Gets all keys of settings in this container. # # \return A set of all keys of settings in this container. def getAllKeys(self): keys = set() for definition in self.definitions: keys |= definition.getAllKeys() return keys ## \copydoc ContainerInterface::getMetaDataEntry # # Reimplemented from ContainerInterface def getMetaDataEntry(self, entry, default=None): return self._metadata.get(entry, default) ## \copydoc ContainerInterface::getProperty # # Reimplemented from ContainerInterface. def getProperty(self, key, property_name): definition = self._getDefinition(key) if not definition: return None try: value = getattr(definition, property_name) if value is None and property_name == "value": value = getattr(definition, "default_value") return value except AttributeError: return None ## \copydoc ContainerInterface::hasProperty # # Reimplemented from ContainerInterface def hasProperty(self, key, property_name): definition = self._getDefinition(key) if not definition: return False return hasattr(definition, property_name) ## This signal is unused since the definition container is immutable, but is provided for API consistency. propertyChanged = Signal() ## \copydoc ContainerInterface::serialize # # TODO: This implementation flattens the definition container, since the # data about inheritance and overrides was lost when deserialising. # # Reimplemented from ContainerInterface def serialize(self): data = {} # The data to write to a JSON file. data["name"] = self.getName() data["version"] = DefinitionContainer.Version data["metadata"] = self.getMetaData() data["settings"] = {} for definition in self.definitions: data["settings"][definition.key] = definition.serialize_to_dict() return json.dumps(data, separators=(", ", ": "), indent=4) # Pretty print the JSON. ## \copydoc ContainerInterface::deserialize # # Reimplemented from ContainerInterface def deserialize(self, serialized): parsed = json.loads(serialized, object_pairs_hook=collections.OrderedDict) self._verifyJson(parsed) # Pre-process the JSON data to include inherited data and overrides if "inherits" in parsed: inherited = self._resolveInheritance(parsed["inherits"]) parsed = self._mergeDicts(inherited, parsed) if "overrides" in parsed: for key, value in parsed["overrides"].items(): setting = self._findInDict(parsed["settings"], key) if setting is None: Logger.log("w", "Unable to override setting %s", key) else: setting.update(value) # If we do not have metadata or settings the file is invalid if not "metadata" in parsed: raise InvalidDefinitionError("Missing required metadata section") if not "settings" in parsed: raise InvalidDefinitionError("Missing required settings section") # Update properties with the data from the JSON self._name = parsed["name"] self._metadata = parsed["metadata"] for key, value in parsed["settings"].items(): definition = SettingDefinition.SettingDefinition( key, self, None, self._i18n_catalog) definition.deserialize(value) self._definitions.append(definition) for definition in self._definitions: self._updateRelations(definition) ## Find definitions matching certain criteria. # # \param kwargs \type{dict} A dictionary of keyword arguments containing key-value pairs which should match properties of the definition. def findDefinitions(self, **kwargs): if len(kwargs) == 1 and "key" in kwargs: # If we are searching for a single definition by exact key, we can speed up things by retrieving from the cache. key = kwargs.get("key") if key in self._definition_cache: return [self._definition_cache[key]] definitions = [] for definition in self._definitions: definitions.extend(definition.findDefinitions(**kwargs)) return definitions # protected: # Load a file from disk, used to handle inheritance and includes def _loadFile(self, file_name): path = Resources.getPath(Resources.DefinitionContainers, file_name + ".def.json") contents = {} with open(path, encoding="utf-8") as f: contents = json.load(f, object_pairs_hook=collections.OrderedDict) self._inherited_files.append(path) return contents # Recursively resolve loading inherited files def _resolveInheritance(self, file_name): result = {} json = self._loadFile(file_name) self._verifyJson(json) if "inherits" in json: inherited = self._resolveInheritance(json["inherits"]) json = self._mergeDicts(inherited, json) return json # Verify that a loaded json matches our basic expectations. def _verifyJson(self, json): if not "version" in json: raise InvalidDefinitionError("Missing required property 'version'") if not "name" in json: raise InvalidDefinitionError("Missing required property 'name'") if json["version"] != self.Version: raise IncorrectDefinitionVersionError( "Definition uses version {0} but expected version {1}".format( json["version"], self.Version)) # Recursively find a key in a dictionary def _findInDict(self, dictionary, key): if key in dictionary: return dictionary[key] for k, v in dictionary.items(): if isinstance(v, dict): item = self._findInDict(v, key) if item is not None: return item # Recursively merge two dictionaries, returning a new dictionary def _mergeDicts(self, first, second): result = copy.deepcopy(first) for key, value in second.items(): if key in result: if isinstance(value, dict): result[key] = self._mergeDicts(result[key], value) else: result[key] = value else: result[key] = value return result # Recursively update relations of settings def _updateRelations(self, definition): for property in SettingDefinition.SettingDefinition.getPropertyNames( SettingDefinition.DefinitionPropertyType.Function): self._processFunction(definition, property) for child in definition.children: self._updateRelations(child) # Create relation objects for all settings used by a certain function def _processFunction(self, definition, property): try: function = getattr(definition, property) except AttributeError: return if not isinstance(function, SettingFunction.SettingFunction): return for setting in function.getUsedSettingKeys(): # Do not create relation on self if setting == definition.key: continue other = self._getDefinition(setting) if not other: continue relation = SettingRelation.SettingRelation( definition, other, SettingRelation.RelationType.RequiresTarget, property) definition.relations.append(relation) relation = SettingRelation.SettingRelation( other, definition, SettingRelation.RelationType.RequiredByTarget, property) other.relations.append(relation) def _getDefinition(self, key): definition = None if key in self._definition_cache: definition = self._definition_cache[key] else: definitions = self.findDefinitions(key=key) if definitions: definition = definitions[0] self._definition_cache[key] = definition return definition
class CloudOutputDeviceManager: """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code. API spec is available on https://api.ultimaker.com/docs/connect/spec/. """ META_CLUSTER_ID = "um_cloud_cluster_id" META_HOST_GUID = "host_guid" META_NETWORK_KEY = "um_network_key" SYNC_SERVICE_NAME = "CloudOutputDeviceManager" # The translation catalog for this device. i18n_catalog = i18nCatalog("cura") # Signal emitted when the list of discovered devices changed. discoveredDevicesChanged = Signal() def __init__(self) -> None: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] # Dictionary containing all the cloud printers loaded in Cura self._um_cloud_printers = {} # type: Dict[str, GlobalStack] self._account = CuraApplication.getInstance().getCuraAPI( ).account # type: Account self._api = CloudApiClient( CuraApplication.getInstance(), on_error=lambda error: Logger.log("e", str(error))) self._account.loginStateChanged.connect(self._onLoginStateChanged) self._removed_printers_message = None # type: Optional[Message] # Ensure we don't start twice. self._running = False self._syncing = False CuraApplication.getInstance().getContainerRegistry( ).containerRemoved.connect(self._printerRemoved) def start(self): """Starts running the cloud output device manager, thus periodically requesting cloud data.""" if self._running: return if not self._account.isLoggedIn: return self._running = True self._getRemoteClusters() self._account.syncRequested.connect(self._getRemoteClusters) def stop(self): """Stops running the cloud output device manager.""" if not self._running: return self._running = False self._onGetRemoteClustersFinished( []) # Make sure we remove all cloud output devices. def refreshConnections(self) -> None: """Force refreshing connections.""" self._connectToActiveMachine() def _onLoginStateChanged(self, is_logged_in: bool) -> None: """Called when the uses logs in or out""" if is_logged_in: self.start() else: self.stop() def _getRemoteClusters(self) -> None: """Gets all remote clusters from the API.""" if self._syncing: return self._syncing = True self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) self._api.getClusters(self._onGetRemoteClustersFinished, self._onGetRemoteClusterFailed) def _onGetRemoteClustersFinished( self, clusters: List[CloudClusterResponse]) -> None: """Callback for when the request for getting the clusters is successful and finished.""" self._um_cloud_printers = { m.getMetaDataEntry(self.META_CLUSTER_ID): m for m in CuraApplication.getInstance().getContainerRegistry(). findContainerStacks(type="machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None) } new_clusters = [] all_clusters = {c.cluster_id: c for c in clusters } # type: Dict[str, CloudClusterResponse] online_clusters = {c.cluster_id: c for c in clusters if c.is_online } # type: Dict[str, CloudClusterResponse] # Add the new printers in Cura. for device_id, cluster_data in all_clusters.items(): if device_id not in self._remote_clusters: new_clusters.append(cluster_data) if device_id in self._um_cloud_printers: # Existing cloud printers may not have the host_guid meta-data entry. If that's the case, add it. if not self._um_cloud_printers[device_id].getMetaDataEntry( self.META_HOST_GUID, None): self._um_cloud_printers[device_id].setMetaDataEntry( self.META_HOST_GUID, cluster_data.host_guid) # If a printer was previously not linked to the account and is rediscovered, mark the printer as linked # to the current account if not parseBool( self._um_cloud_printers[device_id].getMetaDataEntry( META_UM_LINKED_TO_ACCOUNT, "true")): self._um_cloud_printers[device_id].setMetaDataEntry( META_UM_LINKED_TO_ACCOUNT, True) self._onDevicesDiscovered(new_clusters) # Hide the current removed_printers_message, if there is any if self._removed_printers_message: self._removed_printers_message.actionTriggered.disconnect( self._onRemovedPrintersMessageActionTriggered) self._removed_printers_message.hide() # Remove the CloudOutput device for offline printers offline_device_keys = set(self._remote_clusters.keys()) - set( online_clusters.keys()) for device_id in offline_device_keys: self._onDiscoveredDeviceRemoved(device_id) # Handle devices that were previously added in Cura but do not exist in the account anymore (i.e. they were # removed from the account) removed_device_keys = set(self._um_cloud_printers.keys()) - set( all_clusters.keys()) if removed_device_keys: self._devicesRemovedFromAccount(removed_device_keys) if new_clusters or offline_device_keys or removed_device_keys: self.discoveredDevicesChanged.emit() if offline_device_keys: # If the removed device was active we should connect to the new active device self._connectToActiveMachine() self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None: """**Synchronously** create machines for discovered devices Any new machines are made available to the user. May take a long time to complete. As this code needs access to the Application and blocks the GIL, creating a Job for this would not make sense. Shows a Message informing the user of progress. """ new_devices = [] remote_clusters_added = False host_guid_map = { machine.getMetaDataEntry(self.META_HOST_GUID): device_cluster_id for device_cluster_id, machine in self._um_cloud_printers.items() if machine.getMetaDataEntry(self.META_HOST_GUID) } machine_manager = CuraApplication.getInstance().getMachineManager() for cluster_data in clusters: device = CloudOutputDevice(self._api, cluster_data) # If the machine already existed before, it will be present in the host_guid_map if cluster_data.host_guid in host_guid_map: machine = machine_manager.getMachine( device.printerType, {self.META_HOST_GUID: cluster_data.host_guid}) if machine and machine.getMetaDataEntry( self.META_CLUSTER_ID) != device.key: # If the retrieved device has a different cluster_id than the existing machine, bring the existing # machine up-to-date. self._updateOutdatedMachine(outdated_machine=machine, new_cloud_output_device=device) # Create a machine if we don't already have it. Do not make it the active machine. # We only need to add it if it wasn't already added by "local" network or by cloud. if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \ and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key. new_devices.append(device) elif device.getId() not in self._remote_clusters: self._remote_clusters[device.getId()] = device remote_clusters_added = True # If a printer that was removed from the account is re-added, change its metadata to mark it not removed # from the account elif not parseBool( self._um_cloud_printers[device.key].getMetaDataEntry( META_UM_LINKED_TO_ACCOUNT, "true")): self._um_cloud_printers[device.key].setMetaDataEntry( META_UM_LINKED_TO_ACCOUNT, True) # Inform the Cloud printers model about new devices. new_devices_list_of_dicts = [{ "key": d.getId(), "name": d.name, "machine_type": d.printerTypeName, "firmware_version": d.firmwareVersion } for d in new_devices] discovered_cloud_printers_model = CuraApplication.getInstance( ).getDiscoveredCloudPrintersModel() discovered_cloud_printers_model.addDiscoveredCloudPrinters( new_devices_list_of_dicts) if not new_devices: if remote_clusters_added: self._connectToActiveMachine() return # Sort new_devices on online status first, alphabetical second. # Since the first device might be activated in case there is no active printer yet, # it would be nice to prioritize online devices online_cluster_names = { c.friendly_name.lower() for c in clusters if c.is_online and not c.friendly_name is None } new_devices.sort(key=lambda x: ("a{}" if x.name.lower( ) in online_cluster_names else "b{}").format(x.name.lower())) image_path = os.path.join( CuraApplication.getInstance().getPluginRegistry().getPluginPath( "UM3NetworkPrinting") or "", "resources", "svg", "cloud-flow-completed.svg") message = Message(title=self.i18n_catalog.i18ncp( "info:status", "New printer detected from your Ultimaker account", "New printers detected from your Ultimaker account", len(new_devices)), progress=0, lifetime=0, image_source=image_path) message.show() for idx, device in enumerate(new_devices): message_text = self.i18n_catalog.i18nc( "info:status Filled in with printer name and printer model.", "Adding printer {name} ({model}) from your account").format( name=device.name, model=device.printerTypeName) message.setText(message_text) if len(new_devices) > 1: message.setProgress((idx / len(new_devices)) * 100) CuraApplication.getInstance().processEvents() self._remote_clusters[device.getId()] = device # If there is no active machine, activate the first available cloud printer activate = not CuraApplication.getInstance().getMachineManager( ).activeMachine self._createMachineFromDiscoveredDevice(device.getId(), activate=activate) message.setProgress(None) max_disp_devices = 3 if len(new_devices) > max_disp_devices: num_hidden = len(new_devices) - max_disp_devices device_name_list = [ "<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices[0:max_disp_devices] ] device_name_list.append("<li>" + self.i18n_catalog.i18ncp( "info:{0} gets replaced by a number of printers", "... and {0} other", "... and {0} others", num_hidden) + "</li>") device_names = "".join(device_name_list) else: device_names = "".join([ "<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices ]) message_text = self.i18n_catalog.i18nc( "info:status", "Printers added from Digital Factory:" ) + "<ul>" + device_names + "</ul>" message.setText(message_text) def _updateOutdatedMachine( self, outdated_machine: GlobalStack, new_cloud_output_device: CloudOutputDevice) -> None: """ Update the cloud metadata of a pre-existing machine that is rediscovered (e.g. if the printer was removed and re-added to the account) and delete the old CloudOutputDevice related to this machine. :param outdated_machine: The cloud machine that needs to be brought up-to-date with the new data received from the account :param new_cloud_output_device: The new CloudOutputDevice that should be linked to the pre-existing machine :return: None """ old_cluster_id = outdated_machine.getMetaDataEntry( self.META_CLUSTER_ID) outdated_machine.setMetaDataEntry(self.META_CLUSTER_ID, new_cloud_output_device.key) outdated_machine.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) # Cleanup the remainings of the old CloudOutputDevice(old_cluster_id) self._um_cloud_printers[ new_cloud_output_device.key] = self._um_cloud_printers.pop( old_cluster_id) output_device_manager = CuraApplication.getInstance( ).getOutputDeviceManager() if old_cluster_id in output_device_manager.getOutputDeviceIds(): output_device_manager.removeOutputDevice(old_cluster_id) if old_cluster_id in self._remote_clusters: # We need to close the device so that it stops checking for its status self._remote_clusters[old_cluster_id].close() del self._remote_clusters[old_cluster_id] self._remote_clusters[ new_cloud_output_device.key] = new_cloud_output_device def _devicesRemovedFromAccount(self, removed_device_ids: Set[str]) -> None: """ Removes the CloudOutputDevice from the received device ids and marks the specific printers as "removed from account". In addition, it generates a message to inform the user about the printers that are no longer linked to his/her account. The message is not generated if all the printers have been previously reported as not linked to the account. :param removed_device_ids: Set of device ids, whose CloudOutputDevice needs to be removed :return: None """ if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: return # Do not report device ids which have been previously marked as non-linked to the account ignored_device_ids = set() for device_id in removed_device_ids: if not parseBool( self._um_cloud_printers[device_id].getMetaDataEntry( META_UM_LINKED_TO_ACCOUNT, "true")): ignored_device_ids.add(device_id) # Keep the reported_device_ids list in a class variable, so that the message button actions can access it and # take the necessary steps to fulfill their purpose. self.reported_device_ids = removed_device_ids - ignored_device_ids if not self.reported_device_ids: return # Generate message self._removed_printers_message = Message( title=self.i18n_catalog.i18ncp( "info:status", "A cloud connection is not available for a printer", "A cloud connection is not available for some printers", len(self.reported_device_ids))) device_names = "".join([ "<li>{} ({})</li>".format( self._um_cloud_printers[device].name, self._um_cloud_printers[device].definition.name) for device in self.reported_device_ids ]) message_text = self.i18n_catalog.i18ncp( "info:status", "This printer is not linked to the Digital Factory:", "These printers are not linked to the Digital Factory:", len(self.reported_device_ids)) message_text += "<br/><ul>{}</ul><br/>".format(device_names) digital_factory_string = self.i18n_catalog.i18nc( "info:name", "Ultimaker Digital Factory") message_text += self.i18n_catalog.i18nc( "info:status", "To establish a connection, please visit the {website_link}". format(website_link= "<a href='https://digitalfactory.ultimaker.com/'>{}</a>.". format(digital_factory_string))) self._removed_printers_message.setText(message_text) self._removed_printers_message.addAction( "keep_printer_configurations_action", name=self.i18n_catalog.i18nc("@action:button", "Keep printer configurations"), icon="", description= "Keep cloud printers in Ultimaker Cura when not connected to your account.", button_align=Message.ActionButtonAlignment.ALIGN_RIGHT) self._removed_printers_message.addAction( "remove_printers_action", name=self.i18n_catalog.i18nc("@action:button", "Remove printers"), icon="", description= "Remove cloud printer(s) which aren't linked to your account.", button_style=Message.ActionButtonStyle.SECONDARY, button_align=Message.ActionButtonAlignment.ALIGN_LEFT) self._removed_printers_message.actionTriggered.connect( self._onRemovedPrintersMessageActionTriggered) output_device_manager = CuraApplication.getInstance( ).getOutputDeviceManager() # Remove the output device from the printers for device_id in removed_device_ids: device = self._um_cloud_printers.get( device_id, None) # type: Optional[GlobalStack] if not device: continue if device_id in output_device_manager.getOutputDeviceIds(): output_device_manager.removeOutputDevice(device_id) if device_id in self._remote_clusters: del self._remote_clusters[device_id] # Update the printer's metadata to mark it as not linked to the account device.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False) self._removed_printers_message.show() def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: device = self._remote_clusters.pop( device_id, None) # type: Optional[CloudOutputDevice] if not device: return device.close() output_device_manager = CuraApplication.getInstance( ).getOutputDeviceManager() if device.key in output_device_manager.getOutputDeviceIds(): output_device_manager.removeOutputDevice(device.key) def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> None: device = self._remote_clusters[key] if not device: return # Create a new machine. # We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it. new_machine = CuraStackBuilder.createMachine(device.name, device.printerType) if not new_machine: Logger.log("e", "Failed creating a new machine") return self._setOutputDeviceMetadata(device, new_machine) if activate: CuraApplication.getInstance().getMachineManager().setActiveMachine( new_machine.getId()) def _connectToActiveMachine(self) -> None: """Callback for when the active machine was changed by the user""" active_machine = CuraApplication.getInstance().getGlobalContainerStack( ) if not active_machine: return output_device_manager = CuraApplication.getInstance( ).getOutputDeviceManager() stored_cluster_id = active_machine.getMetaDataEntry( self.META_CLUSTER_ID) local_network_key = active_machine.getMetaDataEntry( self.META_NETWORK_KEY) for device in self._remote_clusters.values(): if device.key == stored_cluster_id: # Connect to it if the stored ID matches. self._connectToOutputDevice(device, active_machine) elif local_network_key and device.matchesNetworkKey( local_network_key): # Connect to it if we can match the local network key that was already present. self._connectToOutputDevice(device, active_machine) elif device.key in output_device_manager.getOutputDeviceIds(): # Remove device if it is not meant for the active machine. output_device_manager.removeOutputDevice(device.key) def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry(self.META_HOST_GUID, device.clusterData.host_guid) machine.setMetaDataEntry("group_name", device.name) machine.setMetaDataEntry("group_size", device.clusterSize) digital_factory_string = self.i18n_catalog.i18nc( "info:name", "Ultimaker Digital Factory") digital_factory_link = "<a href='https://digitalfactory.ultimaker.com/'>{digital_factory_string}</a>".format( digital_factory_string=digital_factory_string) removal_warning_string = self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync.").format(printer_name = device.name) \ + "<br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "To remove {printer_name} permanently, visit {digital_factory_link}").format(printer_name = device.name, digital_factory_link = digital_factory_link) \ + "<br><br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "Are you sure you want to remove {printer_name} temporarily?").format(printer_name = device.name) machine.setMetaDataEntry("removal_warning", removal_warning_string) machine.addConfiguredConnectionType(device.connectionType.value) def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None: """Connects to an output device and makes sure it is registered in the output device manager.""" self._setOutputDeviceMetadata(device, machine) if not device.isConnected(): device.connect() output_device_manager = CuraApplication.getInstance( ).getOutputDeviceManager() if device.key not in output_device_manager.getOutputDeviceIds(): output_device_manager.addOutputDevice(device) def _printerRemoved(self, container: ContainerInterface) -> None: """ Callback connected to the containerRemoved signal. Invoked when a cloud printer is removed from Cura to remove the printer's reference from the _remote_clusters. :param container: The ContainerInterface passed to this function whenever the ContainerRemoved signal is emitted :return: None """ if isinstance(container, GlobalStack): container_cluster_id = container.getMetaDataEntry( self.META_CLUSTER_ID, None) if container_cluster_id in self._remote_clusters.keys(): del self._remote_clusters[container_cluster_id] def _onRemovedPrintersMessageActionTriggered( self, removed_printers_message: Message, action: str) -> None: if action == "keep_printer_configurations_action": removed_printers_message.hide() elif action == "remove_printers_action": machine_manager = CuraApplication.getInstance().getMachineManager() remove_printers_ids = { self._um_cloud_printers[i].getId() for i in self.reported_device_ids } all_ids = { m.getId() for m in CuraApplication.getInstance().getContainerRegistry(). findContainerStacks(type="machine") } question_title = self.i18n_catalog.i18nc("@title:window", "Remove printers?") question_content = self.i18n_catalog.i18ncp( "@label", "You are about to remove {0} printer from Cura. This action cannot be undone.\nAre you sure you want to continue?", "You are about to remove {0} printers from Cura. This action cannot be undone.\nAre you sure you want to continue?", len(remove_printers_ids)).format( num_printers=len(remove_printers_ids)) if remove_printers_ids == all_ids: question_content = self.i18n_catalog.i18nc( "@label", "You are about to remove all printers from Cura. This action cannot be undone.\nAre you sure you want to continue?" ) result = QMessageBox.question(None, question_title, question_content) if result == QMessageBox.No: return for machine_cloud_id in self.reported_device_ids: machine_manager.setActiveMachine( self._um_cloud_printers[machine_cloud_id].getId()) machine_manager.removeMachine( self._um_cloud_printers[machine_cloud_id].getId()) removed_printers_message.hide()
class Message: ## Class for displaying messages in the application # \param text Text that needs to be displayed in the message # \param lifetime How long should the message be displayed (in seconds). # if lifetime is 0, it will never automatically be destroyed. # \param dismissible Can the user dismiss the message? # \progress Is there nay progress to be displayed? if -1, it's seen as indeterminate def __init__(self, text="", lifetime=30, dismissable=True, progress=None): #pylint: disable=bad-whitespace super().__init__() self._application = Application.getInstance() self._visible = False self._text = text self._progress = progress # If progress is set to -1, the progress is seen as indeterminate self._max_progress = 100 self._lifetime = lifetime self._lifetime_timer = None self._dismissable = dismissable # Can the message be closed by user? self._actions = [] actionTriggered = Signal() ## Show the message (if not already visible) def show(self): if not self._visible: self._visible = True self._application.showMessageSignal.emit(self) ## Can the message be closed by user? def isDismissable(self): return self._dismissable ## Set the lifetime timer of the message. # This is used by the QT application once the message is shown. # If the lifetime is set to 0, no timer is added. def setTimer(self, timer): self._lifetime_timer = timer if self._lifetime_timer: if self._lifetime: self._lifetime_timer.setInterval(self._lifetime * 1000) self._lifetime_timer.setSingleShot(True) self._lifetime_timer.timeout.connect(self.hide) self._lifetime_timer.start() ## Add an action to the message # Actions are useful for making messages that require input from the user. # \param action_id # \param name The displayed name of the action # \param icon Source of the icon to be used # \param description Description of the item (used for mouse over, etc) def addAction(self, action_id, name, icon, description): self._actions.append({ "action_id": action_id, "name": name, "icon": icon, "description": description }) def getActions(self): return self._actions def setText(self, text): self._text = text def getText(self): return self._text def setMaxProgress(self, max_progress): self._max_progress = max_progress def getMaxProgress(self): return self._max_progress def setProgress(self, progress): self._progress = progress self.progressChanged.emit(self) progressChanged = Signal() def getProgress(self): return self._progress def hide(self): if self._visible: self._visible = False self._application.hideMessageSignal.emit(self)
class QtRenderer(Renderer): def __init__(self) -> None: super().__init__() self._controller = Application.getInstance().getController( ) #type: Controller self._scene = self._controller.getScene() #type: Scene self._initialized = False #type: bool self._light_position = Vector(0, 0, 0) #type: Vector self._background_color = QColor(128, 128, 128) #type: QColor self._viewport_width = 0 # type: int self._viewport_height = 0 # type: int self._window_width = 0 # type: int self._window_height = 0 # type: int self._batches = [] # type: List[RenderBatch] self._quad_buffer = None # type: QOpenGLBuffer self._camera = None # type: Optional[Camera] initialized = Signal() ## Get an integer multiplier that can be used to correct for screen DPI. def getPixelMultiplier(self) -> int: # Standard assumption for screen pixel density is 96 DPI. We use that as baseline to get # a multiplication factor we can use for screens > 96 DPI. return round(UM.Qt.QtApplication.QtApplication.getInstance(). primaryScreen().physicalDotsPerInch() / 96.0) ## Get the list of render batches. def getBatches(self) -> List[RenderBatch]: return self._batches ## Overridden from Renderer. # # This makes sure the added render pass has the right size. def addRenderPass(self, render_pass: "******") -> None: super().addRenderPass(render_pass) render_pass.setSize(self._viewport_width, self._viewport_height) ## Set background color used when rendering. def setBackgroundColor(self, color: QColor) -> None: self._background_color = color def getViewportWidth(self) -> int: return self._viewport_width def getViewportHeight(self) -> int: return self._viewport_height ## Set the viewport size. # # \param width The new width of the viewport. # \param height The new height of the viewport. def setViewportSize(self, width: int, height: int) -> None: self._viewport_width = width self._viewport_height = height for render_pass in self._render_passes: render_pass.setSize(width, height) ## Set the window size. def setWindowSize(self, width: int, height: int) -> None: self._window_width = width self._window_height = height ## Get the window size. # # \return A tuple of (window_width, window_height) def getWindowSize(self) -> Tuple[int, int]: return self._window_width, self._window_height ## Overrides Renderer::beginRendering() def beginRendering(self) -> None: if not self._initialized: self._initialize() self._gl.glViewport(0, 0, self._viewport_width, self._viewport_height) self._gl.glClearColor(self._background_color.redF(), self._background_color.greenF(), self._background_color.blueF(), self._background_color.alphaF()) self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT) self._gl.glClearColor(0.0, 0.0, 0.0, 0.0) ## Overrides Renderer::queueNode() def queueNode(self, node: "SceneNode", **kwargs) -> None: type = kwargs.pop("type", RenderBatch.RenderType.Solid) if kwargs.pop("transparent", False): type = RenderBatch.RenderType.Transparent elif kwargs.pop("overlay", False): type = RenderBatch.RenderType.Overlay shader = kwargs.pop("shader", self._default_material) batch = RenderBatch(shader, type=type, **kwargs) batch.addItem(node.getWorldTransformation(), kwargs.get("mesh", node.getMeshData()), kwargs.pop("uniforms", None)) self._batches.append(batch) ## Overrides Renderer::render() def render(self) -> None: self._batches.sort() for render_pass in self.getRenderPasses(): width, height = render_pass.getSize() self._gl.glViewport(0, 0, width, height) render_pass.render() ## Overrides Renderer::endRendering() def endRendering(self) -> None: self._batches.clear() ## Render a full screen quad (rectangle). # # The function is used to draw render results on. # \param shader The shader to use when rendering. def renderFullScreenQuad(self, shader: "ShaderProgram") -> None: self._gl.glDisable(self._gl.GL_DEPTH_TEST) self._gl.glDisable(self._gl.GL_BLEND) shader.setUniformValue("u_modelViewProjectionMatrix", Matrix()) if OpenGLContext.properties["supportsVertexArrayObjects"]: vao = QOpenGLVertexArrayObject() vao.create() vao.bind() self._quad_buffer.bind() shader.enableAttribute("a_vertex", "vector3f", 0) shader.enableAttribute("a_uvs", "vector2f", 72) self._gl.glDrawArrays(self._gl.GL_TRIANGLES, 0, 6) shader.disableAttribute("a_vertex") shader.disableAttribute("a_uvs") self._quad_buffer.release() def _initialize(self) -> None: supports_vao = OpenGLContext.supportsVertexArrayObjects( ) # fill the OpenGLContext.properties Logger.log("d", "Support for Vertex Array Objects: %s", supports_vao) OpenGL() self._gl = OpenGL.getInstance().getBindingsObject() self._default_material = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "default.shader")) #type: ShaderProgram self._render_passes.add( DefaultPass(self._viewport_width, self._viewport_height)) self._render_passes.add( SelectionPass(self._viewport_width, self._viewport_height)) self._render_passes.add( CompositePass(self._viewport_width, self._viewport_height)) buffer = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer) buffer.create() buffer.bind() buffer.allocate(120) data = numpy.array([ -1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0 ], dtype=numpy.float32).tostring() buffer.write(0, data, len(data)) buffer.release() self._quad_buffer = buffer self._initialized = True self.initialized.emit()
class Message(QObject): class ActionButtonStyle: DEFAULT = 0 LINK = 1 BUTTON_ALIGN_LEFT = 2 BUTTON_ALIGN_RIGHT = 3 ## Class for displaying messages to the user. # Even though the lifetime can be set, in certain cases it can still have a lifetime if nothing happens with the # the message. # We define the following cases; # - A message is dismissible; No timeout (set by lifetime or inactivity) # - A message is set to not dismissible, without progress; We force the dismissible property to be true # - A message is set to not dismissible, with progress; After 30 seconds of no progress updates we hide the message. # \param text Text that needs to be displayed in the message # \param lifetime How long should the message be displayed (in seconds). # if lifetime is 0, it will never automatically be destroyed. # \param dismissible Can the user dismiss the message? # \param title Phrase that will be shown above the message # \progress Is there nay progress to be displayed? if -1, it's seen as indeterminate def __init__(self, text: str = "", lifetime: int = 30, dismissable: bool = True, progress: float = None, title: Optional[str] = None, parent=None, use_inactivity_timer: bool = True) -> None: super().__init__(parent) from UM.Application import Application self._application = Application.getInstance() self._visible = False self._text = text self._progress = progress # If progress is set to -1, the progress is seen as indeterminate self._max_progress = 100 # type: float self._lifetime = lifetime self._lifetime_timer = None # type: Optional[QTimer] self._use_inactivity_timer = use_inactivity_timer self._inactivity_timer = None # type: Optional[QTimer] self._dismissable = dismissable # Can the message be closed by user? if not self._dismissable: # If the message has no lifetime and no progress, it should be dismissible. # this is to prevent messages being permanently visible. if self._lifetime == 0 and self._progress is None: self._dismissable = True self._actions = [] # type: List[Dict[str, Union[str, int]]] self._title = title # We use these signals as QTimers need to be triggered from a qThread. By using signals to pass it, # the events are forced to be on the event loop (which is a qThread) inactivityTimerStop = pyqtSignal() inactivityTimerStart = pyqtSignal() actionTriggered = Signal() def _stopInactivityTimer(self) -> None: if self._inactivity_timer: self._inactivity_timer.stop() def _startInactivityTimer(self) -> None: if self._inactivity_timer: # There is some progress indication, but no lifetime, so the inactivity timer needs to run. if self._progress is not None and self._lifetime == 0: self._inactivity_timer.start() def _onInactivityTriggered(self) -> None: Logger.log("d", "Hiding message because of inactivity") self.hide() ## Show the message (if not already visible) def show(self) -> None: if not self._visible: self._visible = True self._application.showMessageSignal.emit(self) self.inactivityTimerStart.emit() ## Can the message be closed by user? def isDismissable(self) -> bool: return self._dismissable ## Set the lifetime timer of the message. # This is used by the QT application once the message is shown. # If the lifetime is set to 0, no timer is added. # This function is required as the QTimer needs to be created on a QThread. def setLifetimeTimer(self, timer: QTimer) -> None: self._lifetime_timer = timer if self._lifetime_timer: if self._lifetime: self._lifetime_timer.setInterval(self._lifetime * 1000) self._lifetime_timer.setSingleShot(True) self._lifetime_timer.timeout.connect(self.hide) self._lifetime_timer.start() self._startInactivityTimer() ## Set the inactivity timer of the message. # This function is required as the QTimer needs to be created on a QThread. def setInactivityTimer(self, inactivity_timer: QTimer) -> None: if not self._use_inactivity_timer: return self._inactivity_timer = inactivity_timer self._inactivity_timer.setInterval(30 * 1000) self._inactivity_timer.setSingleShot(True) self._inactivity_timer.timeout.connect(self._onInactivityTriggered) self.inactivityTimerStart.connect(self._startInactivityTimer) self.inactivityTimerStop.connect(self._stopInactivityTimer) ## Add an action to the message # Actions are useful for making messages that require input from the user. # \param action_id # \param name The displayed name of the action # \param icon Source of the icon to be used # \param button_style Description the button style (used for Button and Link) # \param button_align Define horizontal position of the action item def addAction(self, action_id: str, name: str, icon: str, description: str, button_style: int = ActionButtonStyle.DEFAULT, button_align: int = ActionButtonStyle.BUTTON_ALIGN_RIGHT): self._actions.append({ "action_id": action_id, "name": name, "icon": icon, "description": description, "button_style": button_style, "button_align": button_align }) ## Get the list of actions to display buttons for on the message. # # Each action is a dictionary with the elements provided in ``addAction``. # # \return A list of actions. def getActions(self) -> List[Dict[str, Union[str, int]]]: return self._actions ## Changes the text on the message. # # \param text The new text for the message. Please ensure that this text # is internationalised. def setText(self, text: str) -> None: self._text = text ## Returns the text in the message. # # \return The text in the message. def getText(self) -> str: return self._text ## Sets the maximum numerical value of the progress bar on the message. # # If the reported progress hits this number, the bar will appear filled. def setMaxProgress(self, max_progress: float) -> None: self._max_progress = max_progress ## Gets the maximum value of the progress bar on the message. # # Note that this is not the _current_ value of the progress bar! # # \return The maximum value of the progress bar on the message. # # \see getProgress def getMaxProgress(self) -> float: return self._max_progress ## Changes the state of the progress bar. # # \param progress The new progress to display to the user. This should be # between 0 and the value of ``getMaxProgress()``. def setProgress(self, progress: float) -> None: if self._progress != progress: self._progress = progress self.progressChanged.emit(self) self.inactivityTimerStart.emit() ## Signal that gets emitted whenever the state of the progress bar on this # message changes. progressChanged = Signal() ## Returns the current progress. # # This should be a value between 0 and the value of ``getMaxProgress()``. # If no progress is set (because the message doesn't have it) None is returned def getProgress(self) -> Optional[float]: return self._progress ## Changes the message title. # # \param text The new title for the message. Please ensure that this text # is internationalised. def setTitle(self, title: str) -> None: self._title = title ## Returns the message title. # # \return The message title. def getTitle(self) -> Optional[str]: return self._title ## Hides this message. # # While the message object continues to exist in memory, it appears to the # user that it is gone. def hide(self, send_signal=True) -> None: if self._visible: self._visible = False self.inactivityTimerStop.emit() if send_signal: self._application.hideMessageSignal.emit(self)
class AuthorizationService: # Emit signal when authentication is completed. onAuthStateChanged = Signal() # Emit signal when authentication failed. onAuthenticationError = Signal() accessTokenChanged = Signal() def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None: self._settings = settings self._auth_helpers = AuthorizationHelpers(settings) self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL) self._auth_data = None # type: Optional[AuthenticationResponse] self._user_profile = None # type: Optional["UserProfile"] self._preferences = preferences self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True) self._unable_to_get_data_message = None # type: Optional[Message] self.onAuthStateChanged.connect(self._authChanged) def _authChanged(self, logged_in): if logged_in and self._unable_to_get_data_message is not None: self._unable_to_get_data_message.hide() def initialize(self, preferences: Optional["Preferences"] = None) -> None: if preferences is not None: self._preferences = preferences if self._preferences: self._preferences.addPreference( self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") ## Get the user profile as obtained from the JWT (JSON Web Token). # If the JWT is not yet parsed, calling this will take care of that. # \return UserProfile if a user is logged in, None otherwise. # \sa _parseJWT def getUserProfile(self) -> Optional["UserProfile"]: if not self._user_profile: # If no user profile was stored locally, we try to get it from JWT. try: self._user_profile = self._parseJWT() except requests.exceptions.ConnectionError: # Unable to get connection, can't login. Logger.logException( "w", "Unable to validate user data with the remote server.") return None if not self._user_profile and self._auth_data: # If there is still no user profile from the JWT, we have to log in again. Logger.log( "w", "The user profile could not be loaded. The user must log in again!" ) self.deleteAuthData() return None return self._user_profile ## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. # \return UserProfile if it was able to parse, None otherwise. def _parseJWT(self) -> Optional["UserProfile"]: if not self._auth_data or self._auth_data.access_token is None: # If no auth data exists, we should always log in again. Logger.log("d", "There was no auth data or access token") return None user_data = self._auth_helpers.parseJWT(self._auth_data.access_token) if user_data: # If the profile was found, we return it immediately. return user_data # The JWT was expired or invalid and we should request a new one. if self._auth_data.refresh_token is None: Logger.log("w", "There was no refresh token in the auth data.") return None self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken( self._auth_data.refresh_token) if not self._auth_data or self._auth_data.access_token is None: Logger.log( "w", "Unable to use the refresh token to get a new access token.") # The token could not be refreshed using the refresh token. We should login again. return None # Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted # from the server already. self._storeAuthData(self._auth_data) return self._auth_helpers.parseJWT(self._auth_data.access_token) ## Get the access token as provided by the repsonse data. def getAccessToken(self) -> Optional[str]: if self._auth_data is None: Logger.log("d", "No auth data to retrieve the access_token from") return None # Check if the current access token is expired and refresh it if that is the case. # We have a fallback on a date far in the past for currently stored auth data in cura.cfg. received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \ if self._auth_data.received_at else datetime(2000, 1, 1) expiry_date = received_at + timedelta( seconds=float(self._auth_data.expires_in or 0) - 60) if datetime.now() > expiry_date: self.refreshAccessToken() return self._auth_data.access_token if self._auth_data else None ## Try to refresh the access token. This should be used when it has expired. def refreshAccessToken(self) -> None: if self._auth_data is None or self._auth_data.refresh_token is None: Logger.log( "w", "Unable to refresh access token, since there is no refresh token." ) return response = self._auth_helpers.getAccessTokenUsingRefreshToken( self._auth_data.refresh_token) if response.success: self._storeAuthData(response) self.onAuthStateChanged.emit(logged_in=True) else: Logger.log("w", "Failed to get a new access token from the server.") self.onAuthStateChanged.emit(logged_in=False) ## Delete the authentication data that we have stored locally (eg; logout) def deleteAuthData(self) -> None: if self._auth_data is not None: self._storeAuthData() self.onAuthStateChanged.emit(logged_in=False) ## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login. def startAuthorizationFlow(self) -> None: Logger.log("d", "Starting new OAuth2 flow...") # Create the tokens needed for the code challenge (PKCE) extension for OAuth2. # This is needed because the CuraDrivePlugin is a untrusted (open source) client. # More details can be found at https://tools.ietf.org/html/rfc7636. verification_code = self._auth_helpers.generateVerificationCode() challenge_code = self._auth_helpers.generateVerificationCodeChallenge( verification_code) # Create the query string needed for the OAuth2 flow. query_string = urlencode({ "client_id": self._settings.CLIENT_ID, "redirect_uri": self._settings.CALLBACK_URL, "scope": self._settings.CLIENT_SCOPES, "response_type": "code", "state": "(.Y.)", "code_challenge": challenge_code, "code_challenge_method": "S512" }) # Open the authorization page in a new browser window. webbrowser.open_new("{}?{}".format(self._auth_url, query_string)) # Start a local web server to receive the callback URL on. self._server.start(verification_code) ## Callback method for the authentication flow. def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: if auth_response.success: self._storeAuthData(auth_response) self.onAuthStateChanged.emit(logged_in=True) else: self.onAuthenticationError.emit( logged_in=False, error_message=auth_response.err_message) self._server.stop() # Stop the web server at all times. ## Load authentication data from preferences. def loadAuthDataFromPreferences(self) -> None: if self._preferences is None: Logger.log( "e", "Unable to load authentication data, since no preference has been set!" ) return try: preferences_data = json.loads( self._preferences.getValue( self._settings.AUTH_DATA_PREFERENCE_KEY)) if preferences_data: self._auth_data = AuthenticationResponse(**preferences_data) # Also check if we can actually get the user profile information. user_profile = self.getUserProfile() if user_profile is not None: self.onAuthStateChanged.emit(logged_in=True) else: if self._unable_to_get_data_message is not None: self._unable_to_get_data_message.hide() self._unable_to_get_data_message = Message( i18n_catalog.i18nc( "@info", "Unable to reach the Ultimaker account server."), title=i18n_catalog.i18nc("@info:title", "Warning")) self._unable_to_get_data_message.addAction( "retry", i18n_catalog.i18nc("@action:button", "Retry"), "[no_icon]", "[no_description]") self._unable_to_get_data_message.actionTriggered.connect( self._onMessageActionTriggered) self._unable_to_get_data_message.show() except ValueError: Logger.logException("w", "Could not load auth data from preferences") ## Store authentication data in preferences. def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None ) -> None: Logger.log("d", "Attempting to store the auth data") if self._preferences is None: Logger.log( "e", "Unable to save authentication data, since no preference has been set!" ) return self._auth_data = auth_data if auth_data: self._user_profile = self.getUserProfile() self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data))) else: self._user_profile = None self._preferences.resetPreference( self._settings.AUTH_DATA_PREFERENCE_KEY) self.accessTokenChanged.emit() def _onMessageActionTriggered(self, _, action): if action == "retry": self.loadAuthDataFromPreferences()
class Controller: def __init__(self, application: "Application"): super().__init__() # Call super to make multiple inheritance work. self._scene = Scene() self._application = application self._is_model_rendering_enabled = True self._active_view = None # type: Optional[View] self._views = {} # type: Dict[str, View] self._active_tool = None # type: Optional[Tool] self._tool_operation_active = False self._tools = {} # type: Dict[str, Tool] self._camera_tool = None self._selection_tool = None self._tools_enabled = True self._active_stage = None self._stages = {} self._input_devices = {} PluginRegistry.addType("stage", self.addStage) PluginRegistry.addType("view", self.addView) PluginRegistry.addType("tool", self.addTool) PluginRegistry.addType("input_device", self.addInputDevice) ## Get the application. # \returns Application \type {Application} def getApplication(self) -> "Application": return self._application ## Add a view by name if it"s not already added. # \param name \type{string} Unique identifier of view (usually the plugin name) # \param view \type{View} The view to be added def addView(self, view: View) -> None: name = view.getPluginId() if name not in self._views: self._views[name] = view view.setRenderer(self._application.getRenderer()) self.viewsChanged.emit() else: Logger.log( "w", "%s was already added to view list. Unable to add it again.", name) ## Request view by name. Returns None if no view is found. # \param name \type{string} Unique identifier of view (usually the plugin name) # \return View \type{View} if name was found, none otherwise. def getView(self, name: str) -> Optional[View]: try: return self._views[name] except KeyError: # No such view Logger.log("e", "Unable to find %s in view list", name) return None ## Return all views. # \return views \type{dict} def getAllViews(self) -> Dict[str, View]: return self._views ## Request active view. Returns None if there is no active view # \return view \type{View} if an view is active, None otherwise. def getActiveView(self) -> Optional[View]: return self._active_view ## Set the currently active view. # \param name \type{string} The name of the view to set as active def setActiveView(self, name: str) -> None: Logger.log("d", "Setting active view to %s", name) try: if self._active_view: self._active_view.event(ViewEvent(Event.ViewDeactivateEvent)) self._active_view = self._views[name] if self._active_view: self._active_view.event(ViewEvent(Event.ViewActivateEvent)) self.activeViewChanged.emit() except KeyError: Logger.log("e", "No view named %s found", name) except Exception as e: Logger.logException( "e", "An exception occurred while switching views: %s", str(e)) def enableModelRendering(self) -> None: self._is_model_rendering_enabled = True def disableModelRendering(self) -> None: self._is_model_rendering_enabled = False def isModelRenderingEnabled(self) -> bool: return self._is_model_rendering_enabled ## Emitted when the list of views changes. viewsChanged = Signal() ## Emitted when the active view changes. activeViewChanged = Signal() ## Add a stage by name if it's not already added. # \param name \type{string} Unique identifier of stage (usually the plugin name) # \param stage \type{Stage} The stage to be added def addStage(self, stage: Stage) -> None: name = stage.getPluginId() if name not in self._stages: self._stages[name] = stage self.stagesChanged.emit() ## Request stage by name. Returns None if no stage is found. # \param name \type{string} Unique identifier of stage (usually the plugin name) # \return Stage \type{Stage} if name was found, none otherwise. def getStage(self, name: str) -> Optional[Stage]: try: return self._stages[name] except KeyError: # No such view Logger.log("e", "Unable to find %s in stage list", name) return None ## Return all stages. # \return stages \type{dict} def getAllStages(self) -> Dict[str, Stage]: return self._stages ## Request active stage. Returns None if there is no active stage # \return stage \type{Stage} if an stage is active, None otherwise. def getActiveStage(self) -> Optional[Stage]: return self._active_stage ## Set the currently active stage. # \param name \type{string} The name of the stage to set as active def setActiveStage(self, name: str) -> None: try: # Don't actually change the stage if it is the current selected one. if self._active_stage != self._stages[name]: previous_stage = self._active_stage Logger.log("d", "Setting active stage to %s", name) self._active_stage = self._stages[name] # If there is no error switching stages, then finish first the previous stage (if it exists) and start the new stage if previous_stage is not None: previous_stage.onStageDeselected() self._active_stage.onStageSelected() self.activeStageChanged.emit() except KeyError: Logger.log("e", "No stage named %s found", name) except Exception as e: Logger.logException( "e", "An exception occurred while switching stages: %s", str(e)) ## Emitted when the list of stages changes. stagesChanged = Signal() ## Emitted when the active stage changes. activeStageChanged = Signal() ## Add an input device (e.g. mouse, keyboard, etc) if it's not already added. # \param device The input device to be added def addInputDevice(self, device: InputDevice) -> None: name = device.getPluginId() if name not in self._input_devices: self._input_devices[name] = device device.event.connect(self.event) else: Logger.log( "w", "%s was already added to input device list. Unable to add it again." % name) ## Request input device by name. Returns None if no device is found. # \param name \type{string} Unique identifier of input device (usually the plugin name) # \return input \type{InputDevice} device if name was found, none otherwise. def getInputDevice(self, name: str) -> Optional[InputDevice]: try: return self._input_devices[name] except KeyError: # No such device Logger.log("e", "Unable to find %s in input devices", name) return None ## Remove an input device from the list of input devices. # Does nothing if the input device is not in the list. # \param name \type{string} The name of the device to remove. def removeInputDevice(self, name: str) -> None: if name in self._input_devices: self._input_devices[name].event.disconnect(self.event) del self._input_devices[name] ## Request tool by name. Returns None if no view is found. # \param name \type{string} Unique identifier of tool (usually the plugin name) # \return tool \type{Tool} if name was found, None otherwise. def getTool(self, name: str) -> Optional["Tool"]: try: return self._tools[name] except KeyError: # No such tool Logger.log("e", "Unable to find %s in tools", name) return None ## Get all tools # \return tools \type{dict} def getAllTools(self) -> Dict[str, "Tool"]: return self._tools ## Add a Tool (transform object, translate object) if its not already added. # \param tool \type{Tool} Tool to be added def addTool(self, tool: "Tool") -> None: name = tool.getPluginId() if name not in self._tools: self._tools[name] = tool tool.operationStarted.connect(self._onToolOperationStarted) tool.operationStopped.connect(self._onToolOperationStopped) self.toolsChanged.emit() else: Logger.log( "w", "%s was already added to tool list. Unable to add it again.", name) def _onToolOperationStarted(self, tool: "Tool") -> None: if not self._tool_operation_active: self._tool_operation_active = True self.toolOperationStarted.emit(tool) def _onToolOperationStopped(self, tool: "Tool") -> None: if self._tool_operation_active: self._tool_operation_active = False self.toolOperationStopped.emit(tool) ## Gets whether a tool is currently in use # \return \type{bool} true if a tool current being used. def isToolOperationActive(self) -> bool: return self._tool_operation_active ## Request active tool. Returns None if there is no active tool # \return Tool \type{Tool} if an tool is active, None otherwise. def getActiveTool(self) -> Optional["Tool"]: return self._active_tool ## Set the current active tool. # The tool can be set by name of the tool or directly passing the tool object. # \param tool \type{Tool} or \type{string} def setActiveTool(self, tool: Optional[Union["Tool", str]]): from UM.Tool import Tool if self._active_tool: self._active_tool.event(ToolEvent(ToolEvent.ToolDeactivateEvent)) if isinstance(tool, Tool) or tool is None: new_tool = tool else: new_tool = self.getTool(tool) tool_changed = False if self._active_tool is not new_tool: self._active_tool = new_tool tool_changed = True if self._active_tool: self._active_tool.event(ToolEvent(ToolEvent.ToolActivateEvent)) from UM.Scene.Selection import Selection # Imported here to prevent a circular dependency. if not self._active_tool and Selection.getCount( ) > 0: # If something is selected, a tool must always be active. if "TranslateTool" in self._tools: self._active_tool = self._tools[ "TranslateTool"] # Then default to the translation tool. self._active_tool.event(ToolEvent(ToolEvent.ToolActivateEvent)) tool_changed = True else: Logger.log( "w", "Controller does not have an active tool and could not default to Translate tool." ) if tool_changed: self.activeToolChanged.emit() ## Emitted when the list of tools changes. toolsChanged = Signal() ## Emitted when a tool changes its enabled state. toolEnabledChanged = Signal() ## Emitted when the active tool changes. activeToolChanged = Signal() ## Emitted whenever a tool starts a longer operation. # # \param tool The tool that started the operation. # \sa Tool::startOperation toolOperationStarted = Signal() ## Emitted whenever a tool stops a longer operation. # # \param tool The tool that stopped the operation. # \sa Tool::stopOperation toolOperationStopped = Signal() ## Get the scene # \return scene \type{Scene} def getScene(self) -> Scene: return self._scene ## Process an event # \param event \type{Event} event to be handle. # The event is first passed to the camera tool, then active tool and finally selection tool. # If none of these events handle it (when they return something that does not evaluate to true) # a context menu signal is emitted. def event(self, event: Event): # First, try to perform camera control if self._camera_tool and self._camera_tool.event(event): return if self._tools and event.type == Event.KeyPressEvent: from UM.Scene.Selection import Selection # Imported here to prevent a circular dependency. if Selection.hasSelection(): for key, tool in self._tools.items(): if tool.getShortcutKey( ) is not None and event.key == tool.getShortcutKey(): self.setActiveTool(tool) if self._selection_tool and self._selection_tool.event(event): return # If we are not doing camera control, pass the event to the active tool. if self._active_tool and self._active_tool.event(event): return if self._active_view: self._active_view.event(event) if event.type == Event.MouseReleaseEvent and MouseEvent.RightButton in event.buttons: self.contextMenuRequested.emit(event.x, event.y) contextMenuRequested = Signal() ## Set the tool used for handling camera controls. # Camera tool is the first tool to receive events. # The tool can be set by name of the tool or directly passing the tool object. # \param tool \type{Tool} or \type{string} # \sa setSelectionTool # \sa setActiveTool def setCameraTool(self, tool: Union["Tool", str]): from UM.Tool import Tool if isinstance(tool, Tool) or tool is None: self._camera_tool = tool else: self._camera_tool = self.getTool(tool) ## Get the camera tool (if any) # \returns camera tool (or none) def getCameraTool(self) -> Optional["Tool"]: return self._camera_tool ## Set the tool used for performing selections. # Selection tool receives its events after camera tool and active tool. # The tool can be set by name of the tool or directly passing the tool object. # \param tool \type{Tool} or \type{string} # \sa setCameraTool # \sa setActiveTool def setSelectionTool(self, tool: Union[str, "Tool"]): from UM.Tool import Tool if isinstance(tool, Tool) or tool is None: self._selection_tool = tool else: self._selection_tool = self.getTool(tool) def getToolsEnabled(self) -> bool: return self._tools_enabled def setToolsEnabled(self, enabled: bool) -> None: self._tools_enabled = enabled # Rotate camera view according defined angle def rotateView(self, coordinate: str = "x", angle: int = 0) -> None: camera = self._scene.getActiveCamera() self._camera_tool.setOrigin(Vector(0, 100, 0)) if coordinate == "home": camera.setPosition(Vector(0, 0, 700)) camera.setPerspective(True) camera.lookAt(Vector(0, 100, 100)) self._camera_tool.rotateCam(0, 0) elif coordinate == "3d": camera.setPosition(Vector(-750, 600, 700)) camera.setPerspective(True) camera.lookAt(Vector(0, 100, 100)) self._camera_tool.rotateCam(0, 0) else: # for comparison is == used, because might not store them at the same location # https://stackoverflow.com/questions/1504717/why-does-comparing-strings-in-python-using-either-or-is-sometimes-produce camera.setPosition(Vector(0, 0, 700)) camera.setPerspective(True) camera.lookAt(Vector(0, 100, 0)) if coordinate == "x": self._camera_tool.rotateCam(angle, 0) elif coordinate == "y": self._camera_tool.rotateCam(0, angle)
class ZeroConfClient: # The discovery protocol name for Ultimaker printers. ZERO_CONF_NAME = u"_ultimaker._tcp.local." # Signals emitted when new services were discovered or removed on the network. addedNetworkCluster = Signal() removedNetworkCluster = Signal() def __init__(self) -> None: self._zero_conf = None # type: Optional[Zeroconf] self._zero_conf_browser = None # type: Optional[ServiceBrowser] self._service_changed_request_queue = None # type: Optional[Queue] self._service_changed_request_event = None # type: Optional[Event] self._service_changed_request_thread = None # type: Optional[Thread] ## The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. # We can also re-schedule the requests when they fail to get detailed service info. # Any new or re-reschedule requests will be appended to the request queue and the thread will process them. def start(self) -> None: self._service_changed_request_queue = Queue() self._service_changed_request_event = Event() self._service_changed_request_thread = Thread( target=self._handleOnServiceChangedRequests, daemon=True) self._service_changed_request_thread.start() self._zero_conf = Zeroconf() self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [self._queueService]) # Cleanup ZeroConf resources. def stop(self) -> None: if self._zero_conf is not None: self._zero_conf.close() self._zero_conf = None if self._zero_conf_browser is not None: self._zero_conf_browser.cancel() self._zero_conf_browser = None ## Handles a change is discovered network services. def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None: item = (zeroconf, service_type, name, state_change) if not self._service_changed_request_queue or not self._service_changed_request_event: return self._service_changed_request_queue.put(item) self._service_changed_request_event.set() ## Callback for when a ZeroConf service has changes. def _handleOnServiceChangedRequests(self) -> None: if not self._service_changed_request_queue or not self._service_changed_request_event: return while True: # Wait for the event to be set self._service_changed_request_event.wait(timeout=5.0) # Stop if the application is shutting down if CuraApplication.getInstance().isShuttingDown(): return self._service_changed_request_event.clear() # Handle all pending requests reschedule_requests = [ ] # A list of requests that have failed so later they will get re-scheduled while not self._service_changed_request_queue.empty(): request = self._service_changed_request_queue.get() zeroconf, service_type, name, state_change = request try: result = self._onServiceChanged(zeroconf, service_type, name, state_change) if not result: reschedule_requests.append(request) except Exception: Logger.logException( "e", "Failed to get service info for [%s] [%s], the request will be rescheduled", service_type, name) reschedule_requests.append(request) # Re-schedule the failed requests if any if reschedule_requests: for request in reschedule_requests: self._service_changed_request_queue.put(request) ## Handler for zeroConf detection. # Return True or False indicating if the process succeeded. # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> bool: if state_change == ServiceStateChange.Added: return self._onServiceAdded(zero_conf, service_type, name) elif state_change == ServiceStateChange.Removed: return self._onServiceRemoved(name) return True ## Handler for when a ZeroConf service was added. def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: # First try getting info from zero-conf cache info = ServiceInfo(service_type, name, properties={}) for record in zero_conf.cache.entries_with_name(name.lower()): info.update_record(zero_conf, time(), record) for record in zero_conf.cache.entries_with_name(info.server): info.update_record(zero_conf, time(), record) if info.address: break # Request more data if info is not complete if not info.address: info = zero_conf.get_service_info(service_type, name) if info: type_of_device = info.properties.get(b"type", None) if type_of_device: if type_of_device == b"printer": address = '.'.join(map(lambda n: str(n), info.address)) self.addedNetworkCluster.emit(str(name), address, info.properties) else: Logger.log( "w", "The type of the found device is '%s', not 'printer'." % type_of_device) else: Logger.log("w", "Could not get information about %s" % name) return False return True ## Handler for when a ZeroConf service was removed. def _onServiceRemoved(self, name: str) -> bool: Logger.log("d", "ZeroConf service removed: %s" % name) self.removedNetworkCluster.emit(str(name)) return True
class NetworkManagerMock: # An enumeration of the supported operations and their code for the network access manager. _OPERATIONS = { "GET": QNetworkAccessManager.GetOperation, "POST": QNetworkAccessManager.PostOperation, "PUT": QNetworkAccessManager.PutOperation, "DELETE": QNetworkAccessManager.DeleteOperation, "HEAD": QNetworkAccessManager.HeadOperation, } # type: Dict[str, int] ## Initializes the network manager mock. def __init__(self) -> None: # A dict with the prepared replies, using the format {(http_method, url): reply} self.replies = {} # type: Dict[Tuple[str, str], MagicMock] self.request_bodies = {} # type: Dict[Tuple[str, str], bytes] # Signals used in the network manager. self.finished = Signal() self.authenticationRequired = Signal() ## Mock implementation of the get, post, put, delete and head methods from the network manager. # Since the methods are very simple and the same it didn't make sense to repeat the code. # \param method: The method being called. # \return The mocked function, if the method name is known. Defaults to the standard getattr function. def __getattr__(self, method: str) -> Any: ## This mock implementation will simply return the reply from the prepared ones. # it raises a KeyError if requests are done without being prepared. def doRequest(request: QNetworkRequest, body: Optional[bytes] = None, *_): key = method.upper(), request.url().toString() if body: self.request_bodies[key] = body return self.replies[key] operation = self._OPERATIONS.get(method.upper()) if operation: return doRequest # the attribute is not one of the implemented methods, default to the standard implementation. return getattr(super(), method) ## Prepares a server reply for the given parameters. # \param method: The HTTP method. # \param url: The URL being requested. # \param status_code: The HTTP status code for the response. # \param response: The response body from the server (generally json-encoded). def prepareReply(self, method: str, url: str, status_code: int, response: Union[bytes, dict]) -> None: reply_mock = MagicMock() reply_mock.url().toString.return_value = url reply_mock.operation.return_value = self._OPERATIONS[method] reply_mock.attribute.return_value = status_code reply_mock.finished = FakeSignal() reply_mock.isFinished.return_value = False reply_mock.readAll.return_value = response if isinstance(response, bytes) else json.dumps(response).encode() self.replies[method, url] = reply_mock Logger.log("i", "Prepared mock {}-response to {} {}", status_code, method, url) ## Gets the request that was sent to the network manager for the given method and URL. # \param method: The HTTP method. # \param url: The URL. def getRequestBody(self, method: str, url: str) -> Optional[bytes]: return self.request_bodies.get((method.upper(), url)) ## Emits the signal that the reply is ready to all prepared replies. def flushReplies(self) -> None: for key, reply in self.replies.items(): Logger.log("i", "Flushing reply to {} {}", *key) reply.isFinished.return_value = True reply.finished.emit() self.finished.emit(reply) self.reset() ## Deletes all prepared replies def reset(self) -> None: self.replies.clear()
class Controller: def __init__(self, application): super().__init__() # Call super to make multiple inheritance work. self._active_tool = None self._tool_operation_active = False self._tools = {} self._input_devices = {} self._active_view = None self._views = {} self._scene = Scene() self._application = application self._camera_tool = None self._selection_tool = None self._tools_enabled = True PluginRegistry.addType("view", self.addView) PluginRegistry.addType("tool", self.addTool) PluginRegistry.addType("input_device", self.addInputDevice) ## Get the application. # \returns Application \type {Application} def getApplication(self): return self._application ## Add a view by name if it"s not already added. # \param name \type{string} Unique identifier of view (usually the plugin name) # \param view \type{View} The view to be added def addView(self, view): name = view.getPluginId() if name not in self._views: self._views[name] = view #view.setController(self) view.setRenderer(self._application.getRenderer()) self.viewsChanged.emit() else: Logger.log( "w", "%s was already added to view list. Unable to add it again.", name) ## Request view by name. Returns None if no view is found. # \param name \type{string} Unique identifier of view (usually the plugin name) # \return View \type{View} if name was found, none otherwise. def getView(self, name): try: return self._views[name] except KeyError: # No such view Logger.log("e", "Unable to find %s in view list", name) return None ## Return all views. # \return views \type{list} def getAllViews(self): return self._views ## Request active view. Returns None if there is no active view # \return view \type{View} if an view is active, None otherwise. def getActiveView(self): return self._active_view ## Set the currently active view. # \param name \type{string} The name of the view to set as active def setActiveView(self, name): Logger.log("d", "Setting active view to %s", name) try: if self._active_view: self._active_view.event(ViewEvent(Event.ViewDeactivateEvent)) self._active_view = self._views[name] if self._active_view: self._active_view.event(ViewEvent(Event.ViewActivateEvent)) self.activeViewChanged.emit() except KeyError: Logger.log("e", "No view named %s found", name) except Exception as e: Logger.log("e", "An exception occurred while switching views", str(e)) ## Emitted when the list of views changes. viewsChanged = Signal() ## Emitted when the active view changes. activeViewChanged = Signal() ## Add an input device (e.g. mouse, keyboard, etc) if it's not already added. # \param device The input device to be added def addInputDevice(self, device): name = device.getPluginId() if name not in self._input_devices: self._input_devices[name] = device device.event.connect(self.event) else: Logger.log( "w", "%s was already added to input device list. Unable to add it again." % name) ## Request input device by name. Returns None if no device is found. # \param name \type{string} Unique identifier of input device (usually the plugin name) # \return input \type{InputDevice} device if name was found, none otherwise. def getInputDevice(self, name): try: return self._input_devices[name] except KeyError: #No such device Logger.log("e", "Unable to find %s in input devices", name) return None ## Remove an input device from the list of input devices. # Does nothing if the input device is not in the list. # \param name \type{string} The name of the device to remove. def removeInputDevice(self, name): if name in self._input_devices: self._input_devices[name].event.disconnect(self.event) del self._input_devices[name] ## Request tool by name. Returns None if no view is found. # \param name \type{string} Unique identifier of tool (usually the plugin name) # \return tool \type{Tool} if name was found, none otherwise. def getTool(self, name): try: return self._tools[name] except KeyError: #No such tool Logger.log("e", "Unable to find %s in tools", name) return None ## Get all tools # \return tools \type{list} def getAllTools(self): return self._tools ## Add a Tool (transform object, translate object) if its not already added. # \param tool \type{Tool} Tool to be added def addTool(self, tool): name = tool.getPluginId() if name not in self._tools: self._tools[name] = tool tool.operationStarted.connect(self._onToolOperationStarted) tool.operationStopped.connect(self._onToolOperationStopped) self.toolsChanged.emit() else: Logger.log( "w", "%s was already added to tool list. Unable to add it again.", name) def _onToolOperationStarted(self, tool): if not self._tool_operation_active: self._tool_operation_active = True self.toolOperationStarted.emit(tool) def _onToolOperationStopped(self, tool): if self._tool_operation_active: self._tool_operation_active = False self.toolOperationStopped.emit(tool) ## Gets whether a tool is currently in use # \return \type{bool} true if a tool current being used. def isToolOperationActive(self): return self._tool_operation_active ## Request active tool. Returns None if there is no active tool # \return Tool \type{Tool} if an tool is active, None otherwise. def getActiveTool(self): return self._active_tool ## Set the current active tool. # The tool can be set by name of the tool or directly passing the tool object. # \param tool \type{Tool} or \type{string} def setActiveTool(self, tool): from UM.Tool import Tool if self._active_tool: self._active_tool.event(ToolEvent(ToolEvent.ToolDeactivateEvent)) if isinstance(tool, Tool) or tool is None: self._active_tool = tool else: self._active_tool = self.getTool(tool) if self._active_tool: self._active_tool.event(ToolEvent(ToolEvent.ToolActivateEvent)) from UM.Scene.Selection import Selection # Imported here to prevent a circular dependency. if not self._active_tool and Selection.getCount( ) > 0: # If something is selected, a tool must always be active. self._active_tool = self._tools[ "TranslateTool"] # Then default to the translation tool. self._active_tool.event(ToolEvent(ToolEvent.ToolActivateEvent)) self.activeToolChanged.emit() ## Emitted when the list of tools changes. toolsChanged = Signal() ## Emitted when a tool changes its enabled state. toolEnabledChanged = Signal() ## Emitted when the active tool changes. activeToolChanged = Signal() ## Emitted whenever a tool starts a longer operation. # # \param tool The tool that started the operation. # \sa Tool::startOperation toolOperationStarted = Signal() ## Emitted whenever a tool stops a longer operation. # # \param tool The tool that stopped the operation. # \sa Tool::stopOperation toolOperationStopped = Signal() ## Get the scene # \return scene \type{Scene} def getScene(self): return self._scene ## Process an event # \param event \type{Event} event to be handle. # The event is first passed to the camera tool, then active tool and finally selection tool. # If none of these events handle it (when they return something that does not evaluate to true) # a context menu signal is emitted. def event(self, event): # First, try to perform camera control if self._camera_tool and self._camera_tool.event(event): return if self._selection_tool and self._selection_tool.event(event): return # If we are not doing camera control, pass the event to the active tool. if self._active_tool and self._active_tool.event(event): return if self._active_view: self._active_view.event(event) if event.type == Event.MouseReleaseEvent and MouseEvent.RightButton in event.buttons: self.contextMenuRequested.emit(event.x, event.y) contextMenuRequested = Signal() ## Set the tool used for handling camera controls. # Camera tool is the first tool to receive events. # The tool can be set by name of the tool or directly passing the tool object. # \param tool \type{Tool} or \type{string} # \sa setSelectionTool # \sa setActiveTool def setCameraTool(self, tool): from UM.Tool import Tool if isinstance(tool, Tool) or tool is None: self._camera_tool = tool else: self._camera_tool = self.getTool(tool) ## Get the camera tool (if any) # \returns camera tool (or none) def getCameraTool(self): return self._camera_tool ## Set the tool used for performing selections. # Selection tool receives its events after camera tool and active tool. # The tool can be set by name of the tool or directly passing the tool object. # \param tool \type{Tool} or \type{string} # \sa setCameraTool # \sa setActiveTool def setSelectionTool(self, tool): from UM.Tool import Tool if isinstance(tool, Tool) or tool is None: self._selection_tool = tool else: self._selection_tool = self.getTool(tool) def getToolsEnabled(self): return self._tools_enabled def setToolsEnabled(self, enabled): self._tools_enabled = enabled
class LocalClusterOutputDeviceManager: META_NETWORK_KEY = "um_network_key" MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0") # The translation catalog for this device. I18N_CATALOG = i18nCatalog("cura") # Signal emitted when the list of discovered devices changed. discoveredDevicesChanged = Signal() def __init__(self) -> None: # Persistent dict containing the networked clusters. self._discovered_devices = {} # type: Dict[str, LocalClusterOutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() # Hook up ZeroConf client. self._zero_conf_client = ZeroConfClient() self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered) self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved) ## Start the network discovery. def start(self) -> None: self._zero_conf_client.start() for address in self._getStoredManualAddresses(): self.addManualDevice(address) ## Stop network discovery and clean up discovered devices. def stop(self) -> None: self._zero_conf_client.stop() for instance_name in list(self._discovered_devices): self._onDiscoveredDeviceRemoved(instance_name) ## Restart discovery on the local network. def startDiscovery(self): self.stop() self.start() ## Add a networked printer manually by address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: api_client = ClusterApiClient(address, lambda error: print(error)) api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback)) ## Remove a manually added networked printer. def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: if device_id not in self._discovered_devices and address is not None: device_id = "manual:{}".format(address) if device_id in self._discovered_devices: address = address or self._discovered_devices[device_id].ipAddress self._onDiscoveredDeviceRemoved(device_id) if address in self._getStoredManualAddresses(): self._removeStoredManualAddress(address) ## Force reset all network device connections. def refreshConnections(self) -> None: self._connectToActiveMachine() ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() stored_device_id = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) for device in self._discovered_devices.values(): if device.key == stored_device_id: # Connect to it if the stored key matches. self._connectToOutputDevice(device, active_machine) elif device.key in output_device_manager.getOutputDeviceIds(): # Remove device if it is not meant for the active machine. CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) ## Callback for when a manual device check request was responded to. def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus, callback: Optional[Callable[[bool, str], None]] = None) -> None: self._onDeviceDiscovered("manual:{}".format(address), address, { b"name": status.name.encode("utf-8"), b"address": address.encode("utf-8"), b"machine": str(status.hardware.get("typeid", "")).encode("utf-8"), b"manual": b"true", b"firmware_version": status.firmware.encode("utf-8"), b"cluster_size": b"1" }) self._storeManualAddress(address) if callback is not None: CuraApplication.getInstance().callLater(callback, True, address) ## Returns a dict of printer BOM numbers to machine types. # These numbers are available in the machine definition already so we just search for them here. @staticmethod def _getPrinterTypeIdentifiers() -> Dict[str, str]: container_registry = CuraApplication.getInstance().getContainerRegistry() ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.") found_machine_type_identifiers = {} # type: Dict[str, str] for machine in ultimaker_machines: machine_bom_number = machine.get("firmware_update_info", {}).get("id", None) machine_type = machine.get("id", None) if machine_bom_number and machine_type: found_machine_type_identifiers[str(machine_bom_number)] = machine_type return found_machine_type_identifiers ## Add a new device. def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: machine_identifier = properties.get(b"machine", b"").decode("utf-8") printer_type_identifiers = self._getPrinterTypeIdentifiers() # Detect the machine type based on the BOM number that is sent over the network. properties[b"printer_type"] = b"Unknown" for bom, p_type in printer_type_identifiers.items(): if machine_identifier.startswith(bom): properties[b"printer_type"] = bytes(p_type, encoding="utf8") break device = LocalClusterOutputDevice(key, address, properties) CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( ip_address=address, key=device.getId(), name=device.getName(), create_callback=self._createMachineFromDiscoveredDevice, machine_type=device.printerType, device=device ) self._discovered_devices[device.getId()] = device self.discoveredDevicesChanged.emit() self._connectToActiveMachine() ## Remove a device. def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice] if not device: return device.close() CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) self.discoveredDevicesChanged.emit() ## Create a machine instance based on the discovered network printer. def _createMachineFromDiscoveredDevice(self, device_id: str) -> None: device = self._discovered_devices.get(device_id) if device is None: return # The newly added machine is automatically activated. CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name) active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return active_machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) active_machine.setMetaDataEntry("group_name", device.name) self._connectToOutputDevice(device, active_machine) CloudFlowMessage(device.ipAddress).show() # Nudge the user to start using Ultimaker Cloud. ## Add an address to the stored preferences. def _storeManualAddress(self, address: str) -> None: stored_addresses = self._getStoredManualAddresses() if address in stored_addresses: return # Prevent duplicates. stored_addresses.append(address) new_value = ",".join(stored_addresses) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value) ## Remove an address from the stored preferences. def _removeStoredManualAddress(self, address: str) -> None: stored_addresses = self._getStoredManualAddresses() try: stored_addresses.remove(address) # Can throw a ValueError new_value = ",".join(stored_addresses) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value) except ValueError: Logger.log("w", "Could not remove address from stored_addresses, it was not there") ## Load the user-configured manual devices from Cura preferences. def _getStoredManualAddresses(self) -> List[str]: preferences = CuraApplication.getInstance().getPreferences() preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") return manual_instances ## Add a device to the current active machine. def _connectToOutputDevice(self, device: UltimakerNetworkedPrinterOutputDevice, machine: GlobalStack) -> None: # Make sure users know that we no longer support legacy devices. if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION: LegacyDeviceNoLongerSupportedMessage().show() return device.connect() machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)
class Tool(PluginObject): def __init__(self): super().__init__() self._controller = UM.Application.Application.getInstance( ).getController() # Circular dependency blah self._enabled = True self._handle = None # type: Optional[ToolHandle] self._locked_axis = None self._drag_plane = None self._drag_start = None self._exposed_properties = [] self._selection_pass = None self._controller.toolEnabledChanged.connect(self._onToolEnabledChanged) self._shortcut_key = None ## Should be emitted whenever a longer running operation is started, like a drag to scale an object. # # \param tool The tool that started the operation. operationStarted = Signal() ## Should be emitted whenever a longer running operation is stopped. # # \param tool The tool that stopped the operation. operationStopped = Signal() propertyChanged = Signal() def getExposedProperties(self): return self._exposed_properties def setExposedProperties(self, *args): self._exposed_properties = args def getShortcutKey(self): return self._shortcut_key ## Handle an event. # \param event \type{Event} The event to handle. # \return \type{bool} true if this event has been handled and requires # no further processing. # \sa Event def event(self, event: Event) -> Optional[bool]: if not self._selection_pass: self._selection_pass = UM.Application.Application.getInstance( ).getRenderer().getRenderPass("selection") if event.type == Event.ToolActivateEvent: if Selection.hasSelection() and self._handle: self._handle.setParent( self.getController().getScene().getRoot()) if event.type == Event.MouseMoveEvent and self._handle: if self._locked_axis: return tool_id = self._selection_pass.getIdAtPosition(event.x, event.y) if self._handle.isAxis(tool_id): self._handle.setActiveAxis(tool_id) else: self._handle.setActiveAxis(None) if event.type == Event.ToolDeactivateEvent and self._handle: self._handle.setParent(None) return False ## Convenience function def getController(self) -> Controller: return self._controller ## Get the enabled state of the tool def getEnabled(self) -> bool: return self._enabled ## Get the associated handle # \return \type{ToolHandle} def getHandle(self) -> Optional[ToolHandle]: return self._handle ## set the associated handle # \param \type{ToolHandle} def setHandle(self, handle: ToolHandle): self._handle = handle def getLockedAxis(self): return self._locked_axis def setLockedAxis(self, axis): self._locked_axis = axis if self._handle: self._handle.setActiveAxis(axis) def getDragPlane(self): return self._drag_plane def setDragPlane(self, plane): self._drag_plane = plane def getDragStart(self): return self._drag_start def setDragStart(self, x, y): self._drag_start = self.getDragPosition(x, y) def getDragPosition(self, x, y): if not self._drag_plane: return None ray = self._controller.getScene().getActiveCamera().getRay(x, y) target = self._drag_plane.intersectsRay(ray) if target: return ray.getPointAlongRay(target) return None def getDragVector(self, x, y): if not self._drag_plane: return None if not self._drag_start: return None drag_end = self.getDragPosition(x, y) if drag_end: return drag_end - self._drag_start return None def _onToolEnabledChanged(self, tool_id: str, enabled: bool): if tool_id == self._plugin_id: self._enabled = enabled
class Script: """Base class for scripts. All scripts should inherit the script class.""" def __init__(self) -> None: super().__init__() self._stack = None # type: Optional[ContainerStack] self._definition = None # type: Optional[DefinitionContainerInterface] self._instance = None # type: Optional[InstanceContainer] def initialize(self) -> None: setting_data = self.getSettingData() self._stack = ContainerStack(stack_id=str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. ## Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: definitions = ContainerRegistry.getInstance().findDefinitionContainers(id=setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] else: self._definition = DefinitionContainer(setting_data["key"]) try: self._definition.deserialize(json.dumps(setting_data)) ContainerRegistry.getInstance().addContainer(self._definition) except ContainerFormatError: self._definition = None return if self._definition is None: return self._stack.addContainer(self._definition) self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) self._instance.setMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default=0)) self._stack.addContainer(self._instance) self._stack.propertyChanged.connect(self._onPropertyChanged) ContainerRegistry.getInstance().addContainer(self._stack) settingsLoaded = Signal() valueChanged = Signal() # Signal emitted whenever a value of a setting is changed def _onPropertyChanged(self, key: str, property_name: str) -> None: if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. # Re-slicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is not None: global_container_stack.propertyChanged.emit(key, property_name) def getSettingData(self) -> Dict[str, Any]: """Needs to return a dict that can be used to construct a settingcategory file. See the example script for an example. It follows the same style / guides as the Uranium settings. Scripts can either override getSettingData directly, or use getSettingDataString to return a string that will be parsed as json. The latter has the benefit over returning a dict in that the order of settings is maintained. """ setting_data_as_string = self.getSettingDataString() setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) return setting_data def getSettingDataString(self) -> str: raise NotImplementedError() def getDefinitionId(self) -> Optional[str]: if self._stack: bottom = self._stack.getBottom() if bottom is not None: return bottom.getId() return None def getStackId(self) -> Optional[str]: if self._stack: return self._stack.getId() return None def getSettingValueByKey(self, key: str) -> Any: """Convenience function that retrieves value of a setting from the stack.""" if self._stack is not None: return self._stack.getProperty(key, "value") return None def getValue(self, line: str, key: str, default = None) -> Any: """Convenience function that finds the value in a line of g-code. When requesting key = x from line "G1 X100" the value 100 is returned. """ if not key in line or (';' in line and line.find(key) > line.find(';')): return default sub_part = line[line.find(key) + 1:] m = re.search('^-?[0-9]+\.?[0-9]*', sub_part) if m is None: return default try: return int(m.group(0)) except ValueError: #Not an integer. try: return float(m.group(0)) except ValueError: #Not a number at all. return default def putValue(self, line: str = "", **kwargs) -> str: """Convenience function to produce a line of g-code. You can put in an original g-code line and it'll re-use all the values in that line. All other keyword parameters are put in the result in g-code's format. For instance, if you put ``G=1`` in the parameters, it will output ``G1``. If you put ``G=1, X=100`` in the parameters, it will output ``G1 X100``. The parameters will be added in order G M T S F X Y Z E. Any other parameters will be added in arbitrary order. :param line: The original g-code line that must be modified. If not provided, an entirely new g-code line will be produced. :return: A line of g-code with the desired parameters filled in. """ # Strip the comment. if ";" in line: comment = line[line.find(";"):] line = line[:line.find(";")] else: comment = "" # Parse the original g-code line and add them to kwargs. for part in line.split(" "): if part == "": continue parameter = part[0] if parameter not in kwargs: value = part[1:] kwargs[parameter] = value # Start writing the new g-code line. line_parts = list() # First add these parameters in order for parameter in ["G", "M", "T", "S", "F", "X", "Y", "Z", "E"]: if parameter in kwargs: value = kwargs.pop(parameter) # get the corresponding value and remove the parameter from kwargs line_parts.append(parameter + str(value)) # Then add the rest of the parameters for parameter, value in kwargs.items(): line_parts.append(parameter + str(value)) # If there was a comment, put it at the end. if comment != "": line_parts.append(comment) # Add spaces and return the new line return " ".join(line_parts) def execute(self, data: List[str]) -> List[str]: """This is called when the script is executed. It gets a list of g-code strings and needs to return a (modified) list. """ raise NotImplementedError()
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin): def __init__(self): super().__init__() self._zero_conf = None self._browser = None self._printers = {} self._cluster_printers_seen = { } # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" self._cluster_api_version = "1" self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" self._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onNetworkRequestFinished) # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces # authentication requests. self._old_printers = [] # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addPrinterSignal.connect(self.addPrinter) self.removePrinterSignal.connect(self.removePrinter) Application.getInstance().globalContainerStackChanged.connect( self.reCheckConnections) # Get list of manual printers from preferences self._preferences = Preferences.getInstance() self._preferences.addPreference( "um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames self._manual_instances = self._preferences.getValue( "um3networkprinting/manual_instances").split(",") self._network_requests_buffer = { } # store api responses until data is complete addPrinterSignal = Signal() removePrinterSignal = Signal() printerListChanged = Signal() ## Start looking for devices on network. def start(self): self.startDiscovery() def startDiscovery(self): self.stop() if self._browser: self._browser.cancel() self._browser = None self._old_printers = [ printer_name for printer_name in self._printers ] self._printers = {} self.printerListChanged.emit() # After network switching, one must make a new instance of Zeroconf # On windows, the instance creation is very fast (unnoticable). Other platforms? self._zero_conf = Zeroconf() self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) # Look for manual instances from preference for address in self._manual_instances: if address: self.addManualPrinter(address) def addManualPrinter(self, address): if address not in self._manual_instances: self._manual_instances.append(address) self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) instance_name = "manual:%s" % address properties = { b"name": address.encode("utf-8"), b"address": address.encode("utf-8"), b"manual": b"true", b"incomplete": b"true" } if instance_name not in self._printers: # Add a preliminary printer instance self.addPrinter(instance_name, address, properties) self.checkManualPrinter(address) self.checkClusterPrinter(address) def removeManualPrinter(self, key, address=None): if key in self._printers: if not address: address = self._printers[key].ipAddress self.removePrinter(key) if address in self._manual_instances: self._manual_instances.remove(address) self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) def checkManualPrinter(self, address): # Check if a printer exists at this address # If a printer responds, it will replace the preliminary printer created above # origin=manual is for tracking back the origin of the call url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name") name_request = QNetworkRequest(url) self._network_manager.get(name_request) def checkClusterPrinter(self, address): cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster") cluster_request = QNetworkRequest(cluster_url) self._network_manager.get(cluster_request) ## Handler for all requests that have finished. def _onNetworkRequestFinished(self, reply): reply_url = reply.url().toString() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: address = reply.url().host() if "origin=manual_name" in reply_url: # Name returned from printer. if status_code == 200: try: system_info = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.JSONDecodeError: Logger.log("e", "Printer returned invalid JSON.") return except UnicodeDecodeError: Logger.log("e", "Printer returned incorrect UTF-8.") return if address not in self._network_requests_buffer: self._network_requests_buffer[address] = {} self._network_requests_buffer[address][ "system"] = system_info elif "origin=check_cluster" in reply_url: if address not in self._network_requests_buffer: self._network_requests_buffer[address] = {} if status_code == 200: # We know it's a cluster printer Logger.log("d", "Cluster printer detected: [%s]", reply.url()) try: cluster_printers_list = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.JSONDecodeError: Logger.log("e", "Printer returned invalid JSON.") return except UnicodeDecodeError: Logger.log("e", "Printer returned incorrect UTF-8.") return self._network_requests_buffer[address]["cluster"] = True self._network_requests_buffer[address][ "cluster_size"] = len(cluster_printers_list) else: Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url()) self._network_requests_buffer[address]["cluster"] = False # Both the system call and cluster call are finished if (address in self._network_requests_buffer and "system" in self._network_requests_buffer[address] and "cluster" in self._network_requests_buffer[address]): instance_name = "manual:%s" % address system_info = self._network_requests_buffer[address]["system"] machine = "unknown" if "variant" in system_info: variant = system_info["variant"] if variant == "Ultimaker 3": machine = "9066" elif variant == "Ultimaker 3 Extended": machine = "9511" properties = { b"name": system_info["name"].encode("utf-8"), b"address": address.encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"), b"manual": b"true", b"machine": machine.encode("utf-8") } if self._network_requests_buffer[address]["cluster"]: properties[ b"cluster_size"] = self._network_requests_buffer[ address]["cluster_size"] if instance_name in self._printers: # Only replace the printer if it is still in the list of (manual) printers self.removePrinter(instance_name) self.addPrinter(instance_name, address, properties) del self._network_requests_buffer[address] ## Stop looking for devices on network. def stop(self): if self._zero_conf is not None: Logger.log("d", "zeroconf close...") self._zero_conf.close() def getPrinters(self): return self._printers def reCheckConnections(self): active_machine = Application.getInstance().getGlobalContainerStack() if not active_machine: return for key in self._printers: if key == active_machine.getMetaDataEntry("um_network_key"): if not self._printers[key].isConnected(): Logger.log("d", "Connecting [%s]..." % key) self._printers[key].connect() self._printers[key].connectionStateChanged.connect( self._onPrinterConnectionStateChanged) else: if self._printers[key].isConnected(): Logger.log("d", "Closing connection [%s]..." % key) self._printers[key].close() self._printers[key].connectionStateChanged.disconnect( self._onPrinterConnectionStateChanged) ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addPrinter(self, name, address, properties): cluster_size = int(properties.get(b"cluster_size", -1)) if cluster_size >= 0: printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice( name, address, properties, self._api_prefix) else: printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice( name, address, properties, self._api_prefix) self._printers[printer.getKey()] = printer self._cluster_printers_seen[printer.getKey( )] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here global_container_stack = Application.getInstance( ).getGlobalContainerStack() if global_container_stack and printer.getKey( ) == global_container_stack.getMetaDataEntry("um_network_key"): if printer.getKey( ) not in self._old_printers: # Was the printer already connected, but a re-scan forced? Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey()) self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect( self._onPrinterConnectionStateChanged) self.printerListChanged.emit() def removePrinter(self, name): printer = self._printers.pop(name, None) if printer: if printer.isConnected(): printer.disconnect() printer.connectionStateChanged.disconnect( self._onPrinterConnectionStateChanged) Logger.log("d", "removePrinter, disconnecting [%s]..." % name) self.printerListChanged.emit() ## Handler for when the connection state of one of the detected printers changes def _onPrinterConnectionStateChanged(self, key): if key not in self._printers: return if self._printers[key].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._printers[key]) else: self.getOutputDeviceManager().removeOutputDevice(key) ## Handler for zeroConf detection def _onServiceChanged(self, zeroconf, service_type, name, state_change): if state_change == ServiceStateChange.Added: Logger.log("d", "Bonjour service added: %s" % name) # First try getting info from zeroconf cache info = ServiceInfo(service_type, name, properties={}) for record in zeroconf.cache.entries_with_name(name.lower()): info.update_record(zeroconf, time.time(), record) for record in zeroconf.cache.entries_with_name(info.server): info.update_record(zeroconf, time.time(), record) if info.address: break # Request more data if info is not complete if not info.address: Logger.log("d", "Trying to get address of %s", name) info = zeroconf.get_service_info(service_type, name) if info: type_of_device = info.properties.get(b"type", None) if type_of_device: if type_of_device == b"printer": address = '.'.join(map(lambda n: str(n), info.address)) self.addPrinterSignal.emit(str(name), address, info.properties) else: Logger.log( "w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) else: Logger.log("w", "Could not get information about %s" % name) elif state_change == ServiceStateChange.Removed: Logger.log("d", "Bonjour service removed: %s" % name) self.removePrinterSignal.emit(str(name)) @pyqtSlot() def openControlPanel(self): Logger.log("d", "Opening print jobs web UI...") selected_device = self.getOutputDeviceManager().getActiveDevice() if isinstance( selected_device, NetworkClusterPrinterOutputDevice. NetworkClusterPrinterOutputDevice): QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))
class CloudPackageChecker(QObject): def __init__(self, application: CuraApplication) -> None: super().__init__() self.discrepancies = Signal() # Emits SubscribedPackagesModel self._application = application # type: CuraApplication self._scope = UltimakerCloudScope(application) self._model = SubscribedPackagesModel() self._application.initializationFinished.connect( self._onAppInitialized) self._i18n_catalog = i18nCatalog("cura") # This is a plugin, so most of the components required are not ready when # this is initialized. Therefore, we wait until the application is ready. def _onAppInitialized(self) -> None: self._package_manager = self._application.getPackageManager() # initial check self._fetchUserSubscribedPackages() # check again whenever the login state changes self._application.getCuraAPI().account.loginStateChanged.connect( self._fetchUserSubscribedPackages) def _fetchUserSubscribedPackages(self) -> None: if self._application.getCuraAPI().account.isLoggedIn: self._getUserPackages() def _handleCompatibilityData(self, json_data) -> None: user_subscribed_packages = [ plugin["package_id"] for plugin in json_data ] user_installed_packages = self._package_manager.getUserInstalledPackages( ) user_dismissed_packages = self._package_manager.getDismissedPackages() if user_dismissed_packages: user_installed_packages += user_dismissed_packages # We check if there are packages installed in Cloud Marketplace but not in Cura marketplace package_discrepancy = list( set(user_subscribed_packages).difference(user_installed_packages)) self._model.setMetadata(json_data) self._model.addDiscrepancies(package_discrepancy) self._model.initialize() if not self._model.hasCompatiblePackages: return None if package_discrepancy: self._handlePackageDiscrepancies() def _handlePackageDiscrepancies(self) -> None: Logger.log( "d", "Discrepancy found between Cloud subscribed packages and Cura installed packages" ) sync_message = Message( self._i18n_catalog.i18nc( "@info:generic", "\nDo you want to sync material and software packages with your account?" ), lifetime=0, title=self._i18n_catalog.i18nc( "@info:title", "Changes detected from your Ultimaker account", )) sync_message.addAction( "sync", name=self._i18n_catalog.i18nc("@action:button", "Sync"), icon="", description= "Sync your Cloud subscribed packages to your local environment.", button_align=Message.ActionButtonAlignment.ALIGN_RIGHT) sync_message.actionTriggered.connect(self._onSyncButtonClicked) sync_message.show() def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None: sync_message.hide() self.discrepancies.emit(self._model) def _getUserPackages(self) -> None: Logger.log("d", "Requesting subscribed packages metadata from server.") url = CloudApiModel.api_url_user_packages self._application.getHttpRequestManager().get( url, callback=self._onUserPackagesRequestFinished, error_callback=self._onUserPackagesRequestFinished, scope=self._scope) def _onUserPackagesRequestFinished( self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: if error is not None or reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) != 200: Logger.log( "w", "Requesting user packages failed, response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) return try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) # Check for errors: if "errors" in json_data: for error in json_data["errors"]: Logger.log("e", "%s", error["title"]) return self._handleCompatibilityData(json_data["data"]) except json.decoder.JSONDecodeError: Logger.log("w", "Received invalid JSON for user packages")
class Application(): ## Init method # # \param name \type{string} The name of the application. # \param version \type{string} Version, formatted as major.minor.rev def __init__(self, name, version, buildtype="", **kwargs): if Application._instance != None: raise ValueError("Duplicate singleton creation") # If the constructor is called and there is no instance, set the instance to self. # This is done because we can't make constructor private Application._instance = self self._application_name = name self._version = version self._buildtype = buildtype os.putenv( "UBUNTU_MENUPROXY", "0" ) # For Ubuntu Unity this makes Qt use its own menu bar rather than pass it on to Unity. Signal._app = self Resources.ApplicationIdentifier = name i18nCatalog.setApplication(self) Resources.addSearchPath( os.path.join(os.path.dirname(sys.executable), "resources")) Resources.addSearchPath( os.path.join(Application.getInstallPrefix(), "share", "uranium", "resources")) Resources.addSearchPath( os.path.join(Application.getInstallPrefix(), "Resources", "uranium", "resources")) Resources.addSearchPath( os.path.join(Application.getInstallPrefix(), "Resources", self.getApplicationName(), "resources")) if not hasattr(sys, "frozen"): Resources.addSearchPath( os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")) self._main_thread = threading.current_thread() super().__init__() # Call super to make multiple inheritance work. self._renderer = None PluginRegistry.addType("backend", self.setBackend) PluginRegistry.addType("logger", Logger.addLogger) PluginRegistry.addType("extension", self.addExtension) preferences = Preferences.getInstance() preferences.addPreference("general/language", "en") preferences.addPreference("general/visible_settings", "") try: preferences.readFromFile( Resources.getPath(Resources.Preferences, self._application_name + ".cfg")) except FileNotFoundError: pass self._controller = Controller(self) self._mesh_file_handler = MeshFileHandler() self._extensions = [] self._backend = None self._output_device_manager = OutputDeviceManager() self._required_plugins = [] self._operation_stack = OperationStack() self._plugin_registry = PluginRegistry.getInstance() self._plugin_registry.addPluginLocation( os.path.join(Application.getInstallPrefix(), "lib", "uranium")) self._plugin_registry.addPluginLocation( os.path.join(os.path.dirname(sys.executable), "plugins")) self._plugin_registry.addPluginLocation( os.path.join(Application.getInstallPrefix(), "Resources", "uranium", "plugins")) self._plugin_registry.addPluginLocation( os.path.join(Application.getInstallPrefix(), "Resources", self.getApplicationName(), "plugins")) # Locally installed plugins local_path = os.path.join( Resources.getStoragePath(Resources.Resources), "plugins") # Ensure the local plugins directory exists try: os.makedirs(local_path) except OSError: pass self._plugin_registry.addPluginLocation(local_path) if not hasattr(sys, "frozen"): self._plugin_registry.addPluginLocation( os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins")) self._plugin_registry.setApplication(self) UM.Settings.ContainerRegistry.setApplication(self) self._parsed_command_line = None self.parseCommandLine() self._visible_messages = [] self._message_lock = threading.Lock() self.showMessageSignal.connect(self.showMessage) self.hideMessageSignal.connect(self.hideMessage) self._global_container_stack = None ## Emitted when the application window was closed and we need to shut down the application applicationShuttingDown = Signal() showMessageSignal = Signal() hideMessageSignal = Signal() globalContainerStackChanged = Signal() def setGlobalContainerStack(self, stack): self._global_container_stack = stack self.globalContainerStackChanged.emit() def getGlobalContainerStack(self): return self._global_container_stack def hideMessage(self, message): raise NotImplementedError def showMessage(self, message): raise NotImplementedError ## Get the version of the application # \returns version \type{string} def getVersion(self): return self._version ## Get the buildtype of the application # \returns version \type{string} def getBuildType(self): return self._buildtype ## Add a message to the visible message list so it will be displayed. # This should only be called by message object itself. # To show a message, simply create it and call its .show() function. # \param message \type{Message} message object # \sa Message::show() #def showMessage(self, message): # with self._message_lock: # if message not in self._visible_messages: # self._visible_messages.append(message) # self.visibleMessageAdded.emit(message) visibleMessageAdded = Signal() ## Remove a message from the visible message list so it will no longer be displayed. # This should only be called by message object itself. # in principle, this should only be called by the message itself (hide) # \param message \type{Message} message object # \sa Message::hide() #def hideMessage(self, message): # with self._message_lock: # if message in self._visible_messages: # self._visible_messages.remove(message) # self.visibleMessageRemoved.emit(message) ## Hide message by ID (as provided by built-in id function) # \param message_id \type{long} def hideMessageById(self, message_id): found_message = None with self._message_lock: for message in self._visible_messages: if id(message) == message_id: found_message = message if found_message is not None: self.hideMessageSignal.emit(found_message) visibleMessageRemoved = Signal() ## Get list of all visible messages # \returns visible_messages \type{list} def getVisibleMessages(self): with self._message_lock: return self._visible_messages ## Function that needs to be overridden by child classes with a list of plugin it needs. def _loadPlugins(self): pass def getCommandLineOption(self, name, default=None): #pylint: disable=bad-whitespace if not self._parsed_command_line: self.parseCommandLine() Logger.log("d", "Command line options: %s", str(self._parsed_command_line)) return self._parsed_command_line.get(name, default) ## Get name of the application. # \returns application_name \type{string} def getApplicationName(self): return self._application_name ## Set name of the application. # \param application_name \type{string} def setApplicationName(self, application_name): self._application_name = application_name def getApplicationLanguage(self): override_lang = os.getenv("URANIUM_LANGUAGE") if override_lang: return override_lang preflang = Preferences.getInstance().getValue("general/language") if preflang: return preflang env_lang = os.getenv("LANGUAGE") if env_lang: return env_lang return "en" ## Application has a list of plugins that it *must* have. If it does not have these, it cannot function. # These plugins can not be disabled in any way. # \returns required_plugins \type{list} def getRequiredPlugins(self): return self._required_plugins ## Set the plugins that the application *must* have in order to function. # \param plugin_names \type{list} List of strings with the names of the required plugins def setRequiredPlugins(self, plugin_names): self._required_plugins = plugin_names ## Set the backend of the application (the program that does the heavy lifting). # \param backend \type{Backend} def setBackend(self, backend): self._backend = backend ## Get the backend of the application (the program that does the heavy lifting). # \returns Backend \type{Backend} def getBackend(self): return self._backend ## Get the PluginRegistry of this application. # \returns PluginRegistry \type{PluginRegistry} def getPluginRegistry(self): return self._plugin_registry ## Get the Controller of this application. # \returns Controller \type{Controller} def getController(self): return self._controller ## Get the MeshFileHandler of this application. # \returns MeshFileHandler \type{MeshFileHandler} def getMeshFileHandler(self): return self._mesh_file_handler def getOperationStack(self): return self._operation_stack def getOutputDeviceManager(self): return self._output_device_manager ## Run the main event loop. # This method should be re-implemented by subclasses to start the main event loop. # \exception NotImplementedError def run(self): raise NotImplementedError("Run must be implemented by application") ## Return an application-specific Renderer object. # \exception NotImplementedError def getRenderer(self): raise NotImplementedError( "getRenderer must be implemented by subclasses.") ## Post a function event onto the event loop. # # This takes a CallFunctionEvent object and puts it into the actual event loop. # \exception NotImplementedError def functionEvent(self, event): raise NotImplementedError( "functionEvent must be implemented by subclasses.") ## Call a function the next time the event loop runs. # # \param function The function to call. # \param args The positional arguments to pass to the function. # \param kwargs The keyword arguments to pass to the function. def callLater(self, function, *args, **kwargs): event = CallFunctionEvent(function, args, kwargs) self.functionEvent(event) ## Get the application"s main thread. def getMainThread(self): return self._main_thread ## Return the singleton instance of the application object @classmethod def getInstance(cls): # Note: Explicit use of class name to prevent issues with inheritance. if Application._instance is None: Application._instance = cls() return Application._instance def parseCommandLine(self): parser = argparse.ArgumentParser(prog=self.getApplicationName()) #pylint: disable=bad-whitespace parser.add_argument("--version", action="version", version="%(prog)s {0}".format(self.getVersion())) parser.add_argument( "--external-backend", dest="external-backend", action="store_true", default=False, help= "Use an externally started backend instead of starting it automatically." ) self.addCommandLineOptions(parser) self._parsed_command_line = vars(parser.parse_args()) ## Can be overridden to add additional command line options to the parser. # # \param parser \type{argparse.ArgumentParser} The parser that will parse the command line. def addCommandLineOptions(self, parser): pass def addExtension(self, extension): self._extensions.append(extension) def getExtensions(self): return self._extensions @staticmethod def getInstallPrefix(): if "python" in os.path.basename(sys.executable): return os.path.abspath( os.path.join(os.path.dirname(sys.argv[0]), "..")) else: return os.path.abspath( os.path.join(os.path.dirname(sys.executable), "..")) _instance = None
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): addUSBOutputDeviceSignal = Signal() progressChanged = pyqtSignal() def __init__(self, parent = None): super().__init__(parent = parent) self._serial_port_list = [] self._usb_output_devices = {} self._usb_output_devices_model = None self._update_thread = threading.Thread(target = self._updateThread) self._update_thread.setDaemon(True) self._check_updates = True Application.getInstance().applicationShuttingDown.connect(self.stop) # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) Application.getInstance().globalContainerStackChanged.connect(self.updateUSBPrinterOutputDevices) # The method updates/reset the USB settings for all connected USB devices def updateUSBPrinterOutputDevices(self): for key, device in self._usb_output_devices.items(): if isinstance(device, USBPrinterOutputDevice.USBPrinterOutputDevice): device.resetDeviceSettings() def start(self): self._check_updates = True self._update_thread.start() def stop(self, store_data: bool = True): self._check_updates = False def _onConnectionStateChanged(self, serial_port): if serial_port not in self._usb_output_devices: return changed_device = self._usb_output_devices[serial_port] if changed_device.connectionState == ConnectionState.connected: self.getOutputDeviceManager().addOutputDevice(changed_device) else: self.getOutputDeviceManager().removeOutputDevice(serial_port) def _updateThread(self): while self._check_updates: container_stack = Application.getInstance().getGlobalContainerStack() if container_stack is None: time.sleep(5) continue port_list = [] # Just an empty list; all USB devices will be removed. if container_stack.getMetaDataEntry("supports_usb_connection"): machine_file_formats = [file_type.strip() for file_type in container_stack.getMetaDataEntry("file_formats").split(";")] if "text/x-gcode" in machine_file_formats: port_list = self.getSerialPortList(only_list_usb=True) self._addRemovePorts(port_list) time.sleep(5) ## Return the singleton instance of the USBPrinterManager @classmethod def getInstance(cls, engine = None, script_engine = None): # Note: Explicit use of class name to prevent issues with inheritance. if USBPrinterOutputDeviceManager._instance is None: USBPrinterOutputDeviceManager._instance = cls() return USBPrinterOutputDeviceManager._instance @pyqtSlot(result = str) def getDefaultFirmwareName(self): # Check if there is a valid global container stack global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: Logger.log("e", "There is no global container stack. Can not update firmware.") self._firmware_view.close() return "" # The bottom of the containerstack is the machine definition machine_id = global_container_stack.getBottom().id machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value") if platform.system() == "Linux": baudrate = 115200 else: baudrate = 250000 # NOTE: The keyword used here is the id of the machine. You can find the id of your machine in the *.json file, eg. # https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json#L2 # The *.hex files are stored at a seperate repository: # https://github.com/Ultimaker/cura-binary-data/tree/master/cura/resources/firmware machine_without_extras = {"bq_witbox" : "MarlinWitbox.hex", "bq_hephestos_2" : "MarlinHephestos2.hex", "ultimaker_original" : "MarlinUltimaker-{baudrate}.hex", "ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex", "ultimaker_original_dual" : "MarlinUltimaker-{baudrate}-dual.hex", "ultimaker2" : "MarlinUltimaker2.hex", "ultimaker2_go" : "MarlinUltimaker2go.hex", "ultimaker2_plus" : "MarlinUltimaker2plus.hex", "ultimaker2_extended" : "MarlinUltimaker2extended.hex", "ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex", } machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex", "ultimaker_original_dual" : "MarlinUltimaker-HBK-{baudrate}-dual.hex", } ##TODO: Add check for multiple extruders hex_file = None if machine_id in machine_without_extras.keys(): # The machine needs to be defined here! if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed: Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id) hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled else: Logger.log("d", "Choosing basic firmware for machine %s.", machine_id) hex_file = machine_without_extras[machine_id] # Return "basic" firmware else: Logger.log("w", "There is no firmware for machine %s.", machine_id) if hex_file: try: return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate)) except FileNotFoundError: Logger.log("w", "Could not find any firmware for machine %s.", machine_id) return "" else: Logger.log("w", "Could not find any firmware for machine %s.", machine_id) return "" ## Helper to identify serial ports (and scan for them) def _addRemovePorts(self, serial_ports): # First, find and add all new or changed keys for serial_port in list(serial_ports): if serial_port not in self._serial_port_list: self.addUSBOutputDeviceSignal.emit(serial_port) # Hack to ensure its created in main thread continue self._serial_port_list = list(serial_ports) for port, device in self._usb_output_devices.items(): if port not in self._serial_port_list: device.close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addOutputDevice(self, serial_port): device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port) device.connectionStateChanged.connect(self._onConnectionStateChanged) self._usb_output_devices[serial_port] = device device.connect() ## Create a list of serial ports on the system. # \param only_list_usb If true, only usb ports are listed def getSerialPortList(self, only_list_usb = False): base_list = [] for port in serial.tools.list_ports.comports(): if not isinstance(port, tuple): port = (port.device, port.description, port.hwid) if only_list_usb and not port[2].startswith("USB"): continue base_list += [port[0]] return list(base_list) _instance = None # type: "USBPrinterOutputDeviceManager"
class SmartSliceCloudProxy(QObject): def __init__(self) -> None: super().__init__() # Primary Button (Slice/Validate/Optimize) self._sliceStatusEnum = SmartSliceCloudStatus.Errors self._sliceStatus = "_Status" self._sliceHint = "_Hint" self._sliceButtonText = "_ButtonText" self._sliceButtonEnabled = False self._sliceButtonVisible = True self._sliceButtonFillWidth = True self._sliceIconImage = "" self._sliceIconVisible = False self._sliceInfoOpen = False self._errors = {} self._job_progress = 0 self._progress_bar_visible = False # Secondary Button (Preview/Cancel) self._secondaryButtonText = "_SecondaryText" self._secondaryButtonFillWidth = False self._secondaryButtonVisible = False self._loadDialog = Dialog.Dialog() self._resultsTableDialog = Dialog.Dialog() # Proxy Values (DO NOT USE DIRECTLY) self._targetFactorOfSafety = 2.0 self._targetMaximalDisplacement = 1.0 self._safetyFactorColor = "#000000" self._maxDisplaceColor = "#000000" # Use-case & Requirements Cache self.reqsMaxDeflect = self._targetMaximalDisplacement # Results table self._resultsTable = ResultTableData() self._resultsTable.updateDisplaySignal.connect( self.updatePropertiesFromResults) self._resultsTable.resultsUpdated.connect(self._resultsTableUpdated) # Properties (mainly) for the sliceinfo widget self._resultSafetyFactor = 0.0 #copy.copy(self._targetFactorOfSafety) self._resultMaximalDisplacement = 0.0 #copy.copy(self._targetMaximalDisplacement) self._resultTimeTotal = Duration() self._resultTimeInfill = Duration() self._resultTimeInnerWalls = Duration() self._resultTimeOuterWalls = Duration() self._resultTimeRetractions = Duration() self._resultTimeSkin = Duration() self._resultTimeSkirt = Duration() self._resultTimeTravel = Duration() self._resultTimes = (self._resultTimeInfill, self._resultTimeInnerWalls, self._resultTimeOuterWalls, self._resultTimeRetractions, self._resultTimeSkin, self._resultTimeSkirt, self._resultTimeTravel) self._percentageTimeInfill = 0.0 self._percentageTimeInnerWalls = 0.0 self._percentageTimeOuterWalls = 0.0 self._percentageTimeRetractions = 0.0 self._percentageTimeSkin = 0.0 self._percentageTimeSkirt = 0.0 self._percentageTimeTravel = 0.0 self.resultTimeInfillChanged.connect(self._onResultTimeChanged) self.resultTimeInnerWallsChanged.connect(self._onResultTimeChanged) self.resultTimeOuterWallsChanged.connect(self._onResultTimeChanged) self.resultTimeRetractionsChanged.connect(self._onResultTimeChanged) self.resultTimeSkinChanged.connect(self._onResultTimeChanged) self.resultTimeSkirtChanged.connect(self._onResultTimeChanged) self.resultTimeTravelChanged.connect(self._onResultTimeChanged) self._materialName = None self._materialCost = 0.0 self._materialLength = 0.0 self._materialWeight = 0.0 # Properties (mainly) for the sliceinfo widget # For main window dialog closeSavePromptClicked = pyqtSignal() escapeSavePromptClicked = pyqtSignal() savePromptClicked = pyqtSignal() # # SLICE BUTTON WINDOW # sliceButtonClicked = pyqtSignal() secondaryButtonClicked = pyqtSignal() sliceStatusChanged = pyqtSignal() sliceStatusEnumChanged = pyqtSignal() sliceButtonFillWidthChanged = pyqtSignal() smartSliceErrorsChanged = pyqtSignal() sliceHintChanged = pyqtSignal() sliceButtonVisibleChanged = pyqtSignal() sliceButtonEnabledChanged = pyqtSignal() sliceButtonTextChanged = pyqtSignal() sliceInfoOpenChanged = pyqtSignal() progressBarVisibleChanged = pyqtSignal() jobProgressChanged = pyqtSignal() secondaryButtonTextChanged = pyqtSignal() secondaryButtonVisibleChanged = pyqtSignal() secondaryButtonFillWidthChanged = pyqtSignal() resultsTableUpdated = pyqtSignal() optimizationResultAppliedToScene = Signal() @pyqtProperty(QObject, constant=True) def loadDialog(self): return self._loadDialog @pyqtProperty(QObject, notify=resultsTableUpdated) def resultsTableDialog(self): return self._resultsTableDialog @pyqtProperty(bool, notify=sliceStatusEnumChanged) def isValidated(self): return self._sliceStatusEnum in SmartSliceCloudStatus.optimizable() @pyqtProperty(bool, notify=sliceStatusEnumChanged) def isOptimized(self): return self._sliceStatusEnum is SmartSliceCloudStatus.Optimized @pyqtProperty(bool, notify=sliceStatusEnumChanged) def errorsExist(self): return self._sliceStatusEnum is SmartSliceCloudStatus.Errors @pyqtProperty(int, notify=sliceStatusEnumChanged) def sliceStatusEnum(self): return self._sliceStatusEnum @sliceStatusEnum.setter def sliceStatusEnum(self, value): if self._sliceStatusEnum is not value: self._sliceStatusEnum = value self.sliceStatusEnumChanged.emit() @pyqtProperty("QVariantMap", notify=smartSliceErrorsChanged) def errors(self) -> Dict[str, str]: return self._errors @errors.setter def errors(self, value: Dict[str, str]): self._errors = value self.smartSliceErrorsChanged.emit() @pyqtProperty(str, notify=sliceStatusChanged) def sliceStatus(self): return self._sliceStatus @sliceStatus.setter def sliceStatus(self, value): if self._sliceStatus is not value: self._sliceStatus = value self.sliceStatusChanged.emit() @pyqtProperty(str, notify=sliceHintChanged) def sliceHint(self): return self._sliceHint @sliceHint.setter def sliceHint(self, value): if self._sliceHint is not value: self._sliceHint = value self.sliceHintChanged.emit() @pyqtProperty(str, notify=sliceButtonTextChanged) def sliceButtonText(self): return self._sliceButtonText @sliceButtonText.setter def sliceButtonText(self, value): if self._sliceButtonText is not value: self._sliceButtonText = value self.sliceButtonTextChanged.emit() @pyqtProperty(bool, notify=sliceInfoOpenChanged) def sliceInfoOpen(self): return self._sliceInfoOpen @sliceInfoOpen.setter def sliceInfoOpen(self, value): if self._sliceInfoOpen is not value: self._sliceInfoOpen = value self.sliceInfoOpenChanged.emit() @pyqtProperty(str, notify=secondaryButtonTextChanged) def secondaryButtonText(self): return self._secondaryButtonText @secondaryButtonText.setter def secondaryButtonText(self, value): if self._secondaryButtonText is not value: self._secondaryButtonText = value self.secondaryButtonTextChanged.emit() @pyqtProperty(bool, notify=sliceButtonEnabledChanged) def sliceButtonEnabled(self): return self._sliceButtonEnabled @sliceButtonEnabled.setter def sliceButtonEnabled(self, value): if self._sliceButtonEnabled is not value: self._sliceButtonEnabled = value self.sliceButtonEnabledChanged.emit() @pyqtProperty(bool, notify=sliceButtonVisibleChanged) def sliceButtonVisible(self): return self._sliceButtonVisible @sliceButtonVisible.setter def sliceButtonVisible(self, value): if self._sliceButtonVisible is not value: self._sliceButtonVisible = value self.sliceButtonVisibleChanged.emit() @pyqtProperty(bool, notify=sliceButtonFillWidthChanged) def sliceButtonFillWidth(self): return self._sliceButtonFillWidth @sliceButtonFillWidth.setter def sliceButtonFillWidth(self, value): if self._sliceButtonFillWidth is not value: self._sliceButtonFillWidth = value self.sliceButtonFillWidthChanged.emit() @pyqtProperty(bool, notify=secondaryButtonFillWidthChanged) def secondaryButtonFillWidth(self): return self._secondaryButtonFillWidth @secondaryButtonFillWidth.setter def secondaryButtonFillWidth(self, value): if self._secondaryButtonFillWidth is not value: self._secondaryButtonFillWidth = value self.secondaryButtonFillWidthChanged.emit() @pyqtProperty(bool, notify=secondaryButtonVisibleChanged) def secondaryButtonVisible(self): return self._secondaryButtonVisible @secondaryButtonVisible.setter def secondaryButtonVisible(self, value): if self._secondaryButtonVisible is not value: self._secondaryButtonVisible = value self.secondaryButtonVisibleChanged.emit() sliceIconImageChanged = pyqtSignal() @pyqtProperty(QUrl, notify=sliceIconImageChanged) def sliceIconImage(self): return self._sliceIconImage @sliceIconImage.setter def sliceIconImage(self, value): if self._sliceIconImage is not value: self._sliceIconImage = value self.sliceIconImageChanged.emit() sliceIconVisibleChanged = pyqtSignal() @pyqtProperty(bool, notify=sliceIconVisibleChanged) def sliceIconVisible(self): return self._sliceIconVisible @sliceIconVisible.setter def sliceIconVisible(self, value): if self._sliceIconVisible is not value: self._sliceIconVisible = value self.sliceIconVisibleChanged.emit() resultSafetyFactorChanged = pyqtSignal() targetSafetyFactorChanged = pyqtSignal() updateTargetUi = pyqtSignal() @pyqtProperty(float, notify=targetSafetyFactorChanged) def targetSafetyFactor(self): return SmartSliceRequirements.getInstance().targetSafetyFactor @pyqtProperty(float, notify=resultSafetyFactorChanged) def resultSafetyFactor(self): return self._resultSafetyFactor @resultSafetyFactor.setter def resultSafetyFactor(self, value): if self._resultSafetyFactor != value: self._resultSafetyFactor = value self.resultSafetyFactorChanged.emit() @pyqtProperty(int, notify=jobProgressChanged) def jobProgress(self): return self._job_progress @jobProgress.setter def jobProgress(self, value): if self._job_progress != value: self._job_progress = value self.jobProgressChanged.emit() @pyqtProperty(bool, notify=progressBarVisibleChanged) def progressBarVisible(self): return self._progress_bar_visible @progressBarVisible.setter def progressBarVisible(self, value): if self._progress_bar_visible is not value: self._progress_bar_visible = value self.progressBarVisibleChanged.emit() # Max Displacement targetMaximalDisplacementChanged = pyqtSignal() resultMaximalDisplacementChanged = pyqtSignal() @pyqtProperty(float, notify=targetMaximalDisplacementChanged) def targetMaximalDisplacement(self): return SmartSliceRequirements.getInstance().maxDisplacement @pyqtProperty(float, notify=resultMaximalDisplacementChanged) def resultMaximalDisplacement(self): return self._resultMaximalDisplacement @resultMaximalDisplacement.setter def resultMaximalDisplacement(self, value): if self._resultMaximalDisplacement != value: self._resultMaximalDisplacement = value self.resultMaximalDisplacementChanged.emit() # # SMART SLICE RESULTS # @pyqtProperty(QAbstractListModel, notify=resultsTableUpdated) def resultsTable(self): return self._resultsTable def _resultsTableUpdated(self): self.resultsTableUpdated.emit() resultTimeTotalChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeTotalChanged) def resultTimeTotal(self): return self._resultTimeTotal @resultTimeTotal.setter def resultTimeTotal(self, value): if self._resultTimeTotal != value: self._resultTimeTotal = value self.resultTimeTotalChanged.emit() resultTimeInfillChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeInfillChanged) def resultTimeInfill(self): return self._resultTimeInfill @resultTimeInfill.setter def resultTimeInfill(self, value): if self._resultTimeInfill != value: self._resultTimeInfill = value self.resultTimeInfillChanged.emit() resultTimeInnerWallsChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeInnerWallsChanged) def resultTimeInnerWalls(self): return self._resultTimeInnerWalls @resultTimeInnerWalls.setter def resultTimeInnerWalls(self, value): if self._resultTimeInnerWalls != value: self._resultTimeInnerWalls = value self.resultTimeInnerWallsChanged.emit() resultTimeOuterWallsChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeOuterWallsChanged) def resultTimeOuterWalls(self): return self._resultTimeOuterWalls @resultTimeOuterWalls.setter def resultTimeOuterWalls(self, value): if self._resultTimeOuterWalls != value: self._resultTimeOuterWalls = value self.resultTimeOuterWallsChanged.emit() resultTimeRetractionsChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeRetractionsChanged) def resultTimeRetractions(self): return self._resultTimeRetractions @resultTimeRetractions.setter def resultTimeRetractions(self, value): if self._resultTimeRetractions != value: self._resultTimeRetractions = value self.resultTimeRetractionsChanged.emit() resultTimeSkinChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeSkinChanged) def resultTimeSkin(self): return self._resultTimeSkin @resultTimeSkin.setter def resultTimeSkin(self, value): if self._resultTimeSkin != value: self._resultTimeSkin = value self.resultTimeSkinChanged.emit() resultTimeSkirtChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeSkirtChanged) def resultTimeSkirt(self): return self._resultTimeSkirt @resultTimeSkirt.setter def resultTimeSkirt(self, value): if self._resultTimeSkirt != value: self._resultTimeSkirt = value self.resultTimeSkirtChanged.emit() resultTimeTravelChanged = pyqtSignal() @pyqtProperty(QObject, notify=resultTimeTravelChanged) def resultTimeTravel(self): return self._resultTimeTravel @resultTimeTravel.setter def resultTimeTravel(self, value): if self._resultTimeTravel != value: self._resultTimeTravel = value self.resultTimeTravelChanged.emit() percentageTimeInfillChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeInfillChanged) def percentageTimeInfill(self): return self._percentageTimeInfill @percentageTimeInfill.setter def percentageTimeInfill(self, value): if not self._percentageTimeInfill == value: self._percentageTimeInfill = value self.percentageTimeInfillChanged.emit() percentageTimeInnerWallsChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeInnerWallsChanged) def percentageTimeInnerWalls(self): return self._percentageTimeInnerWalls @percentageTimeInnerWalls.setter def percentageTimeInnerWalls(self, value): if not self._percentageTimeInnerWalls == value: self._percentageTimeInnerWalls = value self.percentageTimeInnerWallsChanged.emit() percentageTimeOuterWallsChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeOuterWallsChanged) def percentageTimeOuterWalls(self): return self._percentageTimeOuterWalls @percentageTimeOuterWalls.setter def percentageTimeOuterWalls(self, value): if not self._percentageTimeOuterWalls == value: self._percentageTimeOuterWalls = value self.percentageTimeOuterWallsChanged.emit() percentageTimeRetractionsChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeRetractionsChanged) def percentageTimeRetractions(self): return self._percentageTimeRetractions @percentageTimeRetractions.setter def percentageTimeRetractions(self, value): if not self._percentageTimeRetractions == value: self._percentageTimeRetractions = value self.percentageTimeRetractionsChanged.emit() percentageTimeSkinChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeSkinChanged) def percentageTimeSkin(self): return self._percentageTimeSkin @percentageTimeSkin.setter def percentageTimeSkin(self, value): if not self._percentageTimeSkin == value: self._percentageTimeSkin = value self.percentageTimeSkinChanged.emit() percentageTimeSkirtChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeSkirtChanged) def percentageTimeSkirt(self): return self._percentageTimeSkirt @percentageTimeSkirt.setter def percentageTimeSkirt(self, value): if not self._percentageTimeSkirt == value: self._percentageTimeSkirt = value self.percentageTimeSkirtChanged.emit() percentageTimeTravelChanged = pyqtSignal() @pyqtProperty(float, notify=percentageTimeTravelChanged) def percentageTimeTravel(self): return self._percentageTimeTravel @percentageTimeTravel.setter def percentageTimeTravel(self, value): if not self._percentageTimeTravel == value: self._percentageTimeTravel = value self.percentageTimeTravelChanged.emit() def _onResultTimeChanged(self): total_time = 0 #for result_time in self._resultTimes: # total_time += result_time.msecsSinceStartOfDay() total_time += self.resultTimeInfill total_time += self.resultTimeInnerWalls total_time += self.resultTimeOuterWalls total_time += self.resultTimeRetractions total_time += self.resultTimeSkin total_time += self.resultTimeSkirt total_time += self.resultTimeTravel self.percentageTimeInfill = 100.0 / total_time * self.resultTimeInfill self.percentageTimeInnerWalls = 100.0 / total_time * self.resultTimeInnerWalls self.percentageTimeOuterWalls = 100.0 / total_time * self.resultTimeOuterWalls self.percentageTimeRetractions = 100.0 / total_time * self.resultTimeRetractions self.percentageTimeSkin = 100.0 / total_time * self.resultTimeSkin self.percentageTimeSkirt = 100.0 / total_time * self.resultTimeSkirt self.percentageTimeTravel = 100.0 / total_time * self.resultTimeTravel materialNameChanged = pyqtSignal() @pyqtProperty(str, notify=materialNameChanged) def materialName(self): return self._materialName @materialName.setter def materialName(self, value): Logger.log("w", "TODO") self._materialName = value self.materialNameChanged.emit() materialLengthChanged = pyqtSignal() @pyqtProperty(float, notify=materialLengthChanged) def materialLength(self): return self._materialLength @materialLength.setter def materialLength(self, value): if not self._materialLength == value: self._materialLength = value self.materialLengthChanged.emit() materialWeightChanged = pyqtSignal() @pyqtProperty(float, notify=materialWeightChanged) def materialWeight(self): return self._materialWeight @materialWeight.setter def materialWeight(self, value): if not self._materialWeight == value: self._materialWeight = value self.materialWeightChanged.emit() materialCostChanged = pyqtSignal() @pyqtProperty(float, notify=materialCostChanged) def materialCost(self): return self._materialCost @materialCost.setter def materialCost(self, value): if not self._materialCost == value: self._materialCost = value self.materialCostChanged.emit() # # UI Color Handling # safetyFactorColorChanged = pyqtSignal() maxDisplaceColorChanged = pyqtSignal() @pyqtProperty(str, notify=safetyFactorColorChanged) def safetyFactorColor(self): return self._safetyFactorColor @safetyFactorColor.setter def safetyFactorColor(self, value): self._safetyFactorColor = value @pyqtProperty(str, notify=maxDisplaceColorChanged) def maxDisplaceColor(self): return self._maxDisplaceColor @maxDisplaceColor.setter def maxDisplaceColor(self, value): self._maxDisplaceColor = value def updateColorSafetyFactor(self): # Update Safety Factor Color if self._resultSafetyFactor > self.targetSafetyFactor: self.safetyFactorColor = SmartSlicePropertyColor.WarningColor elif self._resultSafetyFactor < self.targetSafetyFactor: self.safetyFactorColor = SmartSlicePropertyColor.ErrorColor else: self.safetyFactorColor = SmartSlicePropertyColor.SuccessColor # Override if part has gone through optimization if self._sliceStatusEnum == SmartSliceCloudStatus.Optimized: self.safetyFactorColor = SmartSlicePropertyColor.SuccessColor self.safetyFactorColorChanged.emit() def updateColorMaxDisplacement(self): # Update Maximal Displacement Color if self._resultMaximalDisplacement < self.targetMaximalDisplacement: self.maxDisplaceColor = SmartSlicePropertyColor.WarningColor elif self._resultMaximalDisplacement > self.targetMaximalDisplacement: self.maxDisplaceColor = SmartSlicePropertyColor.ErrorColor else: self.maxDisplaceColor = SmartSlicePropertyColor.SuccessColor # Override if part has gone through optimization if self._sliceStatusEnum == SmartSliceCloudStatus.Optimized: self.maxDisplaceColor = SmartSlicePropertyColor.SuccessColor self.maxDisplaceColorChanged.emit() def updateColorUI(self): self.updateColorSafetyFactor() self.updateColorMaxDisplacement() # Updates the properties from a job setup def updatePropertiesFromJob(self, job: pywim.smartslice.job.Job, callback): select_tool = SmartSliceSelectTool.getInstance() select_tool.updateFromJob(job, callback) requirements = SmartSliceRequirements.getInstance() requirements.targetSafetyFactor = job.optimization.min_safety_factor requirements.maxDisplacement = job.optimization.max_displacement # Updates the properties def updatePropertiesFromResults(self, result): self.resultSafetyFactor = result[ResultsTableHeader.Strength.value] self.resultMaximalDisplacement = result[ ResultsTableHeader.Displacement.value] self.resultTimeTotal = Duration(result[ResultsTableHeader.Time.value]) # TODO: Modify the block as soon as we have the single print times again! #self.resultTimeInfill = QTime(1, 0, 0, 0) #self.resultTimeInnerWalls = QTime(0, 20, 0, 0) #self.resultTimeOuterWalls = QTime(0, 15, 0, 0) #self.resultTimeRetractions = QTime(0, 5, 0, 0) #self.resultTimeSkin = QTime(0, 10, 0, 0) #self.resultTimeSkirt = QTime(0, 1, 0, 0) #self.resultTimeTravel = QTime(0, 30, 0, 0) self.materialLength = result[ResultsTableHeader.Length.value] self.materialWeight = result[ResultsTableHeader.Mass.value] self.materialCost = result[ResultsTableHeader.Cost.value] # Below is commented out because we don't necessarily need it right now. # We aren't sending multiple materials to optimize, so the material here # won't change. And this assignment causes the "Lose Validation Results" # pop-up to show. #self.materialName = material_extra_info[3][pos] if self._sliceStatusEnum == SmartSliceCloudStatus.Optimized: result_id = result[ResultsTableHeader.Rank.value] - 1 self.updateSceneFromOptimizationResult( self._resultsTable.analyses[result_id]) def updateStatusFromResults(self, job: pywim.smartslice.job.Job, results: pywim.smartslice.result.Result): if job: if job.type == pywim.smartslice.job.JobType.validation: if results: self.optimizationStatus() self.sliceInfoOpen = True else: self._sliceStatusEnum = SmartSliceCloudStatus.ReadyToVerify else: self.optimizationStatus() self.sliceInfoOpen = True else: self._sliceStatusEnum = SmartSliceCloudStatus.Errors Application.getInstance().activityChanged.emit() def optimizationStatus(self): req_tool = SmartSliceRequirements.getInstance() if req_tool.maxDisplacement > self.resultMaximalDisplacement and req_tool.targetSafetyFactor < self.resultSafetyFactor: self._sliceStatusEnum = SmartSliceCloudStatus.Overdimensioned elif req_tool.maxDisplacement <= self.resultMaximalDisplacement or req_tool.targetSafetyFactor >= self.resultSafetyFactor: self._sliceStatusEnum = SmartSliceCloudStatus.Underdimensioned else: self._sliceStatusEnum = SmartSliceCloudStatus.Optimized def updateSceneFromOptimizationResult( self, analysis: pywim.smartslice.result.Analysis): our_only_node = getPrintableNodes()[0] active_extruder = getNodeActiveExtruder(our_only_node) # TODO - Move this into a common class or function to apply an am.Config to GlobalStack/ExtruderStack if analysis.print_config.infill: infill_density = analysis.print_config.infill.density infill_pattern = analysis.print_config.infill.pattern if infill_pattern is None or infill_pattern == pywim.am.InfillType.unknown: infill_pattern = pywim.am.InfillType.grid infill_pattern_name = SmartSliceJobHandler.INFILL_SMARTSLICE_CURA[ infill_pattern] extruder_dict = { "wall_line_count": analysis.print_config.walls, "top_layers": analysis.print_config.top_layers, "bottom_layers": analysis.print_config.bottom_layers, "infill_sparse_density": analysis.print_config.infill.density, "infill_pattern": infill_pattern_name } Logger.log("d", "Optimized extruder settings: {}".format(extruder_dict)) for key, value in extruder_dict.items(): if value is not None: active_extruder.setProperty(key, "value", value, set_from_cache=True) Application.getInstance().getMachineManager( ).forceUpdateAllSettings() self.optimizationResultAppliedToScene.emit() # Remove any modifier meshes which are present from a previous result mod_meshes = getModifierMeshes() if len(mod_meshes) > 0: op = GroupedOperation() for node in mod_meshes: node.addDecorator(SmartSliceRemovedDecorator()) op.addOperation(RemoveSceneNodeOperation(node)) op.push() Application.getInstance().getController().getScene( ).sceneChanged.emit(node) # Add in the new modifier meshes for modifier_mesh in analysis.modifier_meshes: # Building the scene node modifier_mesh_node = CuraSceneNode() modifier_mesh_node.setName("SmartSliceMeshModifier") modifier_mesh_node.setSelectable(True) modifier_mesh_node.setCalculateBoundingBox(True) # Use the data from the SmartSlice engine to translate / rotate / scale the mod mesh parent_transformation = our_only_node.getLocalTransformation() modifier_mesh_transform_matrix = parent_transformation.multiply( Matrix(modifier_mesh.transform)) modifier_mesh_node.setTransformation( modifier_mesh_transform_matrix) # Building the mesh # # Preparing the data from pywim for MeshBuilder modifier_mesh_vertices = [[v.x, v.y, v.z] for v in modifier_mesh.vertices] modifier_mesh_indices = [[triangle.v1, triangle.v2, triangle.v3] for triangle in modifier_mesh.triangles] # Doing the actual build modifier_mesh_data = MeshBuilder() modifier_mesh_data.setVertices( numpy.asarray(modifier_mesh_vertices, dtype=numpy.float32)) modifier_mesh_data.setIndices( numpy.asarray(modifier_mesh_indices, dtype=numpy.int32)) modifier_mesh_data.calculateNormals() modifier_mesh_node.setMeshData(modifier_mesh_data.build()) modifier_mesh_node.calculateBoundingBoxMesh() active_build_plate = Application.getInstance( ).getMultiBuildPlateModel().activeBuildPlate modifier_mesh_node.addDecorator( BuildPlateDecorator(active_build_plate)) modifier_mesh_node.addDecorator(SliceableObjectDecorator()) modifier_mesh_node.addDecorator(SmartSliceAddedDecorator()) bottom = modifier_mesh_node.getBoundingBox().bottom z_offset_decorator = ZOffsetDecorator() z_offset_decorator.setZOffset(bottom) modifier_mesh_node.addDecorator(z_offset_decorator) stack = modifier_mesh_node.callDecoration("getStack") settings = stack.getTop() modifier_mesh_node_infill_pattern = SmartSliceJobHandler.INFILL_SMARTSLICE_CURA[ modifier_mesh.print_config.infill.pattern] definition_dict = { "infill_mesh": True, "infill_pattern": modifier_mesh_node_infill_pattern, "infill_sparse_density": modifier_mesh.print_config.infill.density, "wall_line_count": modifier_mesh.print_config.walls, "top_layers": modifier_mesh.print_config.top_layers, "bottom_layers": modifier_mesh.print_config.bottom_layers, } Logger.log( "d", "Optimized modifier mesh settings: {}".format(definition_dict)) for key, value in definition_dict.items(): if value is not None: definition = stack.getSettingDefinition(key) new_instance = SettingInstance(definition, settings) new_instance.setProperty("value", value) new_instance.resetState( ) # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) op = GroupedOperation() # First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent op.addOperation( AddSceneNodeOperation( modifier_mesh_node, Application.getInstance().getController().getScene(). getRoot())) op.addOperation( SetParentOperation( modifier_mesh_node, Application.getInstance().getController().getScene(). getRoot())) op.push() # emit changes and connect error tracker Application.getInstance().getController().getScene( ).sceneChanged.emit(modifier_mesh_node)
class Scene: def __init__(self) -> None: super().__init__() from UM.Scene.SceneNode import SceneNode self._root = SceneNode(name = "Root") self._root.setCalculateBoundingBox(False) self._connectSignalsRoot() self._active_camera = None # type: Optional[Camera] self._ignore_scene_changes = False self._lock = threading.Lock() # Watching file for changes. self._file_watcher = QFileSystemWatcher() self._file_watcher.fileChanged.connect(self._onFileChanged) def _connectSignalsRoot(self) -> None: self._root.transformationChanged.connect(self.sceneChanged) self._root.childrenChanged.connect(self.sceneChanged) self._root.meshDataChanged.connect(self.sceneChanged) def _disconnectSignalsRoot(self) -> None: self._root.transformationChanged.disconnect(self.sceneChanged) self._root.childrenChanged.disconnect(self.sceneChanged) self._root.meshDataChanged.disconnect(self.sceneChanged) def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None: if self._ignore_scene_changes != ignore_scene_changes: self._ignore_scene_changes = ignore_scene_changes if self._ignore_scene_changes: self._disconnectSignalsRoot() else: self._connectSignalsRoot() ## Acquire the global scene lock. # # This will prevent any read or write actions on the scene from other threads, # assuming those threads also properly acquire the lock. Most notably, this # prevents the rendering thread from rendering the scene while it is changing. # Deprecated, use getSceneLock() instead. @deprecated("Please use the getSceneLock instead", "3.3") def acquireLock(self) -> None: self._lock.acquire() ## Release the global scene lock. # Deprecated, use getSceneLock() instead. @deprecated("Please use the getSceneLock instead", "3.3") def releaseLock(self) -> None: self._lock.release() ## Gets the global scene lock. # # Use this lock to prevent any read or write actions on the scene from other threads, # assuming those threads also properly acquire the lock. Most notably, this # prevents the rendering thread from rendering the scene while it is changing. def getSceneLock(self) -> threading.Lock: return self._lock ## Get the root node of the scene. def getRoot(self) -> "SceneNode": return self._root ## Change the root node of the scene def setRoot(self, node: "SceneNode") -> None: if self._root != node: if not self._ignore_scene_changes: self._disconnectSignalsRoot() self._root = node if not self._ignore_scene_changes: self._connectSignalsRoot() self.rootChanged.emit() rootChanged = Signal() ## Get the camera that should be used for rendering. def getActiveCamera(self) -> Optional[Camera]: return self._active_camera def getAllCameras(self) -> List[Camera]: cameras = [] for node in BreadthFirstIterator(self._root): # type: ignore if isinstance(node, Camera): cameras.append(node) return cameras ## Set the camera that should be used for rendering. # \param name The name of the camera to use. def setActiveCamera(self, name: str) -> None: camera = self.findCamera(name) if camera: self._active_camera = camera else: Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name) ## Signal that is emitted whenever something in the scene changes. # \param object The object that triggered the change. sceneChanged = Signal() ## Find an object by id. # # \param object_id The id of the object to search for, as returned by the python id() method. # # \return The object if found, or None if not. def findObject(self, object_id: int) -> Optional["SceneNode"]: for node in BreadthFirstIterator(self._root): # type: ignore if id(node) == object_id: return node return None def findCamera(self, name: str) -> Optional[Camera]: for node in BreadthFirstIterator(self._root): # type: ignore if isinstance(node, Camera) and node.getName() == name: return node return None ## Add a file to be watched for changes. # \param file_path The path to the file that must be watched. def addWatchedFile(self, file_path: str) -> None: # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.addPath(file_path) ## Remove a file so that it will no longer be watched for changes. # \param file_path The path to the file that must no longer be watched. def removeWatchedFile(self, file_path: str) -> None: # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive if not Platform.isWindows(): self._file_watcher.removePath(file_path) ## Triggered whenever a file is changed that we currently have loaded. def _onFileChanged(self, file_path: str) -> None: if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0: # File doesn't exist any more, or it is empty return # Multiple nodes may be loaded from the same file at different stages. Reload them all. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # To find which nodes to reload when files have changed. modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path] # type: ignore if modified_nodes: self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)), title = i18n_catalog.i18nc("@info:title", "File has been modified")) self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk.")) self._reload_callback = functools.partial(self._reloadNodes, modified_nodes) self._reload_message.actionTriggered.connect(self._reload_callback) self._reload_message.show() ## Reloads a list of nodes after the user pressed the "Reload" button. # \param nodes The list of nodes that needs to be reloaded. # \param message The message that triggered the action to reload them. # \param action The button that triggered the action to reload them. def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None: if action != "reload": return self._reload_message.hide() for node in nodes: meshdata = node.getMeshData() if meshdata: filename = meshdata.getFileName() if not filename or not os.path.isfile(filename): # File doesn't exist any more. continue job = ReadMeshJob(filename) self._reload_finished_callback = functools.partial(self._reloadJobFinished, node) job.finished.connect(self._reload_finished_callback) job.start() ## Triggered when reloading has finished. # # This then puts the resulting mesh data in the node. def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None: for node in job.getResult(): mesh_data = node.getMeshData() if mesh_data: replaced_node.setMeshData(mesh_data) else: Logger.log("w", "Could not find a mesh in reloaded node.")
class Backend(PluginObject): def __init__(self): super().__init__() # Call super to make multiple inheritance work. self._supported_commands = {} self._message_handlers = {} self._socket = None self._port = 49674 self._process = None self._backend_log = [] self._backend_log_max_lines = None Application.getInstance().callLater(self._createSocket) processingProgress = Signal() backendStateChange = Signal() backendConnected = Signal() backendQuit = Signal() ## \brief Start the backend / engine. # Runs the engine, this is only called when the socket is fully opened & ready to accept connections def startEngine(self): try: command = self.getEngineCommand() if not command: self._createSocket() return if not self._backend_log_max_lines: self._backend_log = [] # Double check that the old process is indeed killed. if self._process is not None: self._process.terminate() Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) self._process = self._runEngineProcess(command) if self._process is None: # Failed to start engine. return Logger.log("i", "Started engine process: %s", self.getEngineCommand()[0]) self._backendLog(bytes("Calling engine with: %s\n" % self.getEngineCommand(), "utf-8")) t = threading.Thread(target = self._storeOutputToLogThread, args = (self._process.stdout,)) t.daemon = True t.start() t = threading.Thread(target = self._storeStderrToLogThread, args = (self._process.stderr,)) t.daemon = True t.start() except FileNotFoundError: Logger.logException("e", "Unable to find backend executable: %s", self.getEngineCommand()[0]) def close(self): if self._socket: while self._socket.getState() == Arcus.SocketState.Opening: sleep(0.1) self._socket.close() def _backendLog(self, line): Logger.log('d', "[Backend] " + str(line, encoding="utf-8").strip()) self._backend_log.append(line) ## Get the logging messages of the backend connection. # \returns def getLog(self): if self._backend_log_max_lines and type(self._backend_log_max_lines) == int: while len(self._backend_log) >= self._backend_log_max_lines: del(self._backend_log[0]) return self._backend_log ## \brief Convert byte array containing 3 floats per vertex def convertBytesToVerticeList(self, data): result = [] if not (len(data) % 12): if data is not None: for index in range(0, int(len(data) / 12)): # For each 12 bits (3 floats) result.append(struct.unpack("fff", data[index * 12: index * 12 + 12])) return result else: Logger.log("e", "Data length was incorrect for requested type") return None ## \brief Convert byte array containing 6 floats per vertex def convertBytesToVerticeWithNormalsList(self,data): result = [] if not (len(data) % 24): if data is not None: for index in range(0,int(len(data)/24)): # For each 24 bits (6 floats) result.append(struct.unpack("ffffff", data[index * 24: index * 24 + 24])) return result else: Logger.log("e", "Data length was incorrect for requested type") return None ## Get the command used to start the backend executable def getEngineCommand(self): return [Preferences.getInstance().getValue("backend/location"), "--port", str(self._socket.getPort())] ## Start the (external) backend process. def _runEngineProcess(self, command_list): kwargs = {} if sys.platform == "win32": su = subprocess.STARTUPINFO() su.dwFlags |= subprocess.STARTF_USESHOWWINDOW su.wShowWindow = subprocess.SW_HIDE kwargs["startupinfo"] = su kwargs["creationflags"] = 0x00004000 # BELOW_NORMAL_PRIORITY_CLASS try: return subprocess.Popen(command_list, stdin = subprocess.DEVNULL, stdout = subprocess.PIPE, stderr = subprocess.PIPE, **kwargs) except PermissionError: Logger.log("e", "Couldn't start back-end: No permission to execute process.") def _storeOutputToLogThread(self, handle): while True: line = handle.readline() if line == b"": self.backendQuit.emit() break self._backendLog(line) def _storeStderrToLogThread(self, handle): while True: line = handle.readline() if line == b"": break self._backendLog(line) ## Private socket state changed handler. def _onSocketStateChanged(self, state): self._logSocketState(state) if state == Arcus.SocketState.Listening: if not Application.getInstance().getCommandLineOption("external-backend", False): self.startEngine() elif state == Arcus.SocketState.Connected: Logger.log("d", "Backend connected on port %s", self._port) self.backendConnected.emit() ## Debug function created to provide more info for CURA-2127 def _logSocketState(self, state): if state == Arcus.SocketState.Listening: Logger.log("d", "Socket state changed to Listening") elif state == Arcus.SocketState.Connecting: Logger.log("d", "Socket state changed to Connecting") elif state == Arcus.SocketState.Connected: Logger.log("d", "Socket state changed to Connected") elif state == Arcus.SocketState.Error: Logger.log("d", "Socket state changed to Error") elif state == Arcus.SocketState.Closing: Logger.log("d", "Socket state changed to Closing") elif state == Arcus.SocketState.Closed: Logger.log("d", "Socket state changed to Closed") ## Private message handler def _onMessageReceived(self): message = self._socket.takeNextMessage() if message.getTypeName() not in self._message_handlers: Logger.log("e", "No handler defined for message of type %s", message.getTypeName()) return self._message_handlers[message.getTypeName()](message) ## Private socket error handler def _onSocketError(self, error): if error.getErrorCode() == Arcus.ErrorCode.BindFailedError: self._port += 1 Logger.log("d", "Socket was unable to bind to port, increasing port number to %s", self._port) elif error.getErrorCode() == Arcus.ErrorCode.ConnectionResetError: Logger.log("i", "Backend crashed or closed.") elif error.getErrorCode() == Arcus.ErrorCode.Debug: Logger.log("d", "Socket debug: %s", str(error)) return else: Logger.log("w", "Unhandled socket error %s", str(error)) self._createSocket() ## Creates a socket and attaches listeners. def _createSocket(self, protocol_file): if self._socket: Logger.log("d", "Previous socket existed. Closing that first.") # temp debug logging self._socket.stateChanged.disconnect(self._onSocketStateChanged) self._socket.messageReceived.disconnect(self._onMessageReceived) self._socket.error.disconnect(self._onSocketError) # Hack for (at least) Linux. If the socket is connecting, the close will deadlock. while self._socket.getState() == Arcus.SocketState.Opening: sleep(0.1) # If the error occurred due to parsing, both connections believe that connection is okay. # So we need to force a close. self._socket.close() self._socket = SignalSocket() self._socket.stateChanged.connect(self._onSocketStateChanged) self._socket.messageReceived.connect(self._onMessageReceived) self._socket.error.connect(self._onSocketError) if Platform.isWindows(): # On Windows, the Protobuf DiskSourceTree does stupid things with paths. # So convert to forward slashes here so it finds the proto file properly. protocol_file = protocol_file.replace("\\", "/") if not self._socket.registerAllMessageTypes(protocol_file): Logger.log("e", "Could not register Uranium protocol messages: %s", self._socket.getLastError()) if Application.getInstance().getCommandLineOption("external-backend", False): Logger.log("i", "Listening for backend connections on %s", self._port) self._socket.listen("127.0.0.1", self._port)