Exemple #1
0
def test_signal():
    test = SignalReceiver()

    signal = Signal(type = Signal.Direct)
    signal.connect(test.slot)
    signal.emit()

    assert test.getEmitCount() == 1
Exemple #2
0
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
Exemple #3
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
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
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()
Exemple #8
0
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
Exemple #9
0
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())
Exemple #12
0
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
Exemple #13
0
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
Exemple #14
0
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
Exemple #15
0
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)
Exemple #16
0
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()
Exemple #17
0
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)
Exemple #18
0
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)
Exemple #20
0
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
Exemple #22
0
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
Exemple #23
0
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()
Exemple #24
0
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)
Exemple #25
0
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()
Exemple #26
0
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()
Exemple #28
0
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)
Exemple #29
0
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()
Exemple #31
0
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
Exemple #32
0
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)
Exemple #33
0
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
Exemple #34
0
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()))
Exemple #36
0
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")
Exemple #37
0
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
Exemple #38
0
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"
Exemple #39
0
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)
Exemple #40
0
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.")
Exemple #41
0
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)