示例#1
0
class DiscrepanciesPresenter(QObject):
    def __init__(self, app: QtApplication) -> None:
        super().__init__(app)

        self.packageMutations = Signal()  #  Emits SubscribedPackagesModel

        self._app = app
        self._package_manager = app.getPackageManager()
        self._dialog = None  # type: Optional[QObject]
        self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"

    def present(self, plugin_path: str,
                model: SubscribedPackagesModel) -> None:
        path = os.path.join(plugin_path, self._compatibility_dialog_path)
        self._dialog = self._app.createQmlComponent(path, {
            "subscribedPackagesModel": model,
            "handler": self
        })
        assert self._dialog
        self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))

    @pyqtSlot("QVariant", str)
    def dismissIncompatiblePackage(self, model: SubscribedPackagesModel,
                                   package_id: str) -> None:
        model.dismissPackage(package_id)  # update the model to update the view
        self._package_manager.dismissPackage(
            package_id
        )  # adds this package_id as dismissed in the user config file

    def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None:
        # For now, all compatible packages presented to the user should be installed.
        # Later, we might remove items for which the user unselected the package
        model.setItems(model.getCompatiblePackages())
        self.packageMutations.emit(model)
示例#2
0
class DiscrepanciesPresenter(QObject):
    def __init__(self, app: QtApplication) -> None:
        super().__init__(app)

        self.packageMutations = Signal()  #  Emits SubscribedPackagesModel

        self._app = app
        self._package_manager = app.getPackageManager()
        self._dialog = None  # type: Optional[QObject]
        self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"

    def present(self, plugin_path: str,
                model: SubscribedPackagesModel) -> None:
        path = os.path.join(plugin_path, self._compatibility_dialog_path)
        self._dialog = self._app.createQmlComponent(path, {
            "subscribedPackagesModel": model,
            "handler": self
        })
        assert self._dialog
        self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))

    def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None:
        # If there are incompatible packages - automatically dismiss them
        if model.getIncompatiblePackages():
            self._package_manager.dismissAllIncompatiblePackages(
                model.getIncompatiblePackages())
        # For now, all compatible packages presented to the user should be installed.
        # Later, we might remove items for which the user unselected the package
        if model.getCompatiblePackages():
            model.setItems(model.getCompatiblePackages())
            self.packageMutations.emit(model)
示例#3
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
示例#4
0
def test_signal():
    test = SignalReceiver()

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

    assert test.getEmitCount() == 1
示例#5
0
def test_signal():
    test = SignalReceiver()

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

    assert test.getEmitCount() == 1
示例#6
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
示例#7
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
示例#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
示例#9
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
示例#10
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
示例#11
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
示例#12
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
示例#13
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
示例#14
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
示例#15
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
示例#16
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
示例#17
0
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"

    def present(self, plugin_path: str,
                packages: Dict[str, Dict[str, str]]) -> None:
        """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]
        """
        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)
示例#18
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
示例#19
0
class SmartSliceCloudConnector(QObject):
    debug_save_smartslice_package_preference = "smartslice/debug_save_smartslice_package"
    debug_save_smartslice_package_location = "smartslice/debug_save_smartslice_package_location"

    class SubscriptionTypes(Enum):
        subscriptionExpired = 0
        trialExpired = 1

    def __init__(self, proxy: SmartSliceCloudProxy, extension):
        super().__init__()

        # Variables
        self._job = None
        self._jobs = {}
        self._current_job = 0
        self._jobs[self._current_job] = None

        # Proxy
        #General
        self._proxy = proxy
        self._proxy.sliceButtonClicked.connect(self.onSliceButtonClicked)
        self._proxy.secondaryButtonClicked.connect(self.onSecondaryButtonClicked)

        self.extension = extension

        # Debug stuff
        self.app_preferences = Application.getInstance().getPreferences()
        self.app_preferences.addPreference(self.debug_save_smartslice_package_preference, False)
        default_save_smartslice_package_location = str(Path.home())
        self.app_preferences.addPreference(self.debug_save_smartslice_package_location, default_save_smartslice_package_location)
        self.debug_save_smartslice_package_message = None

        # Executing a set of function when some activitiy has changed
        Application.getInstance().activityChanged.connect(self._onApplicationActivityChanged)

        #  Machines / Extruders
        self.activeMachine = None
        self.propertyHandler = None # SmartSlicePropertyHandler
        self.smartSliceJobHandle = None

        Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)

        self._confirmDialog = []
        self.confirming = False

        self.saveSmartSliceJob = Signal()

        self.api_connection = SmartSliceAPIClient(self)

    onSmartSlicePrepared = pyqtSignal()

    @property
    def cloudJob(self) -> SmartSliceCloudJob:
        if len(self._jobs) > 0:
            return self._jobs[self._current_job]

        return None

    def addJob(self, job_type: pywim.smartslice.job.JobType):

        self.propertyHandler._cancelChanges = False
        self._current_job += 1

        if job_type == pywim.smartslice.job.JobType.optimization:
            self._jobs[self._current_job] = SmartSliceCloudOptimizeJob(self)
        else:
            self._jobs[self._current_job] = SmartSliceCloudVerificationJob(self)

        self._jobs[self._current_job]._id = self._current_job
        self._jobs[self._current_job].finished.connect(self._onJobFinished)

    def cancelCurrentJob(self):
        if self._jobs[self._current_job] and not self._jobs[self._current_job].canceled:

            # Cancel the job if it has been submitted
            if self._jobs[self._current_job].api_job_id:
                self.api_connection.cancelJob(self._jobs[self._current_job].api_job_id)

            if not self._jobs[self._current_job].canceled:
                self.status = SmartSliceCloudStatus.Cancelling
                self.updateStatus()

            self._jobs[self._current_job].cancel()
            self._jobs[self._current_job].canceled = True
            self._jobs[self._current_job].setResult(None)

    # Resets all of the tracked properties and jobs
    def clearJobs(self):

        # Cancel the running job (if any)
        if len(self._jobs) > 0 and self._jobs[self._current_job] and self._jobs[self._current_job].isRunning():
            self.cancelCurrentJob()

        # Clear out the jobs
        self._jobs.clear()
        self._current_job = 0
        self._jobs[self._current_job] = None

    def _onSaveDebugPackage(self, messageId: str, actionId: str) -> None:
        dummy_job = SmartSliceCloudVerificationJob(self)
        if self.status == SmartSliceCloudStatus.ReadyToVerify:
            dummy_job.job_type = pywim.smartslice.job.JobType.validation
        elif self.status in SmartSliceCloudStatus.optimizable():
            dummy_job.job_type = pywim.smartslice.job.JobType.optimization
        else:
            Logger.log("e", "DEBUG: This is not a defined state. Provide all input to create the debug package.")
            return

        jobname = Application.getInstance().getPrintInformation().jobName
        debug_filename = "{}_smartslice.3mf".format(jobname)
        debug_filedir = self.app_preferences.getValue(self.debug_save_smartslice_package_location)
        dummy_job = dummy_job.prepareJob(filename=debug_filename, filedir=debug_filedir)

    def getProxy(self, engine=None, script_engine=None):
        return self._proxy

    def getAPI(self, engine=None, script_engine=None):
        return self.api_connection

    def _onEngineCreated(self):
        self.activeMachine = Application.getInstance().getMachineManager().activeMachine
        self.propertyHandler = SmartSlicePropertyHandler(self)
        self.smartSliceJobHandle = SmartSliceJobHandler(self.propertyHandler)

        self.onSmartSlicePrepared.emit()
        self.propertyHandler.cacheChanges() # Setup Cache

        Application.getInstance().getMachineManager().printerConnectedStatusChanged.connect(self._refreshMachine)

        if self.app_preferences.getValue(self.debug_save_smartslice_package_preference):
            self.debug_save_smartslice_package_message = Message(
                title="[DEBUG] SmartSlicePlugin",
                text= "Click on the button below to generate a debug package",
                lifetime= 0,
            )
            self.debug_save_smartslice_package_message.addAction("", i18n_catalog.i18nc("@action", "Save package"), "", "")
            self.debug_save_smartslice_package_message.actionTriggered.connect(self._onSaveDebugPackage)
            self.debug_save_smartslice_package_message.show()

    def _refreshMachine(self):
        self.activeMachine = Application.getInstance().getMachineManager().activeMachine

    def updateSliceWidget(self):
        if self.status is SmartSliceCloudStatus.Errors:
            self._proxy.sliceStatus = ""
            self._proxy.sliceHint = ""
            self._proxy.sliceButtonText = "Validate"
            self._proxy.sliceButtonEnabled = False
            self._proxy.sliceButtonVisible = True
            self._proxy.sliceButtonFillWidth = True
            self._proxy.secondaryButtonVisible = False
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.Cancelling:
            self._proxy.sliceStatus = ""
            self._proxy.sliceHint = ""
            self._proxy.sliceButtonText = "Cancelling"
            self._proxy.sliceButtonEnabled = False
            self._proxy.sliceButtonVisible = True
            self._proxy.sliceButtonFillWidth = True
            self._proxy.secondaryButtonVisible = False
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.ReadyToVerify:
            self._proxy.sliceStatus = ""
            self._proxy.sliceHint = ""
            self._proxy.sliceButtonText = "Validate"
            self._proxy.sliceButtonEnabled = True
            self._proxy.sliceButtonVisible = True
            self._proxy.sliceButtonFillWidth = True
            self._proxy.secondaryButtonVisible = False
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.BusyValidating:
            self._proxy.sliceStatus = "Validating..."
            self._proxy.sliceHint = ""
            self._proxy.secondaryButtonText = "Cancel"
            self._proxy.sliceButtonVisible = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = True
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.Underdimensioned:
            self._proxy.sliceStatus = "Requirements not met!"
            self._proxy.sliceHint = "Optimize to meet requirements?"
            self._proxy.sliceButtonText = "Optimize"
            self._proxy.secondaryButtonText = "Preview"
            self._proxy.sliceButtonEnabled = True
            self._proxy.sliceButtonVisible = True
            self._proxy.sliceButtonFillWidth = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = False
            self._proxy.sliceInfoOpen = True
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.Overdimensioned:
            self._proxy.sliceStatus = "Part appears overdesigned"
            self._proxy.sliceHint = "Optimize to reduce print time and material?"
            self._proxy.sliceButtonText = "Optimize"
            self._proxy.secondaryButtonText = "Preview"
            self._proxy.sliceButtonEnabled = True
            self._proxy.sliceButtonVisible = True
            self._proxy.sliceButtonFillWidth = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = False
            self._proxy.sliceInfoOpen = True
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.BusyOptimizing:
            self._proxy.sliceStatus = "Optimizing...&nbsp;&nbsp;&nbsp;&nbsp;(<i>Remaining Time: calculating</i>)"
            self._proxy.sliceHint = ""
            self._proxy.secondaryButtonText = "Cancel"
            self._proxy.sliceButtonVisible = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = True
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = True
        elif self.status is SmartSliceCloudStatus.Optimized:
            self._proxy.sliceStatus = ""
            self._proxy.sliceHint = ""
            self._proxy.secondaryButtonText = "Preview"
            self._proxy.sliceButtonVisible = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = True
            self._proxy.sliceInfoOpen = True
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.Queued:
            self._proxy.sliceStatus = "Queued..."
            self._proxy.sliceHint = ""
            self._proxy.secondaryButtonText = "Cancel"
            self._proxy.sliceButtonVisible = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = True
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        elif self.status is SmartSliceCloudStatus.RemoveModMesh:
            self._proxy.sliceStatus = ""
            self._proxy.sliceHint = ""
            self._proxy.secondaryButtonText = "Cancel"
            self._proxy.sliceButtonVisible = False
            self._proxy.secondaryButtonVisible = True
            self._proxy.secondaryButtonFillWidth = True
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0
        else:
            self._proxy.sliceStatus = "Unknown status"
            self._proxy.sliceHint = "Sorry, something went wrong!"
            self._proxy.sliceButtonText = "..."
            self._proxy.sliceButtonEnabled = False
            self._proxy.sliceButtonVisible = True
            self._proxy.secondaryButtonVisible = False
            self._proxy.secondaryButtonFillWidth = False
            self._proxy.sliceInfoOpen = False
            self._proxy.progressBarVisible = False
            self._proxy.jobProgress = 0

        # Setting icon path
        stage_path = PluginRegistry.getInstance().getPluginPath("SmartSlicePlugin")
        stage_images_path = os.path.join(stage_path, "stage", "images")
        icon_done_green = os.path.join(stage_images_path, "done_green.png")
        icon_error_red = os.path.join(stage_images_path, "error_red.png")
        icon_warning_yellow = os.path.join(stage_images_path, "warning_yellow.png")
        current_icon = icon_done_green
        if self.status is SmartSliceCloudStatus.Overdimensioned:
            current_icon = icon_warning_yellow
        elif self.status is SmartSliceCloudStatus.Underdimensioned:
            current_icon = icon_error_red
        current_icon = QUrl.fromLocalFile(current_icon)
        self._proxy.sliceIconImage = current_icon

        # Setting icon visibiltiy
        if self.status in (SmartSliceCloudStatus.Optimized,) + SmartSliceCloudStatus.optimizable():
            self._proxy.sliceIconVisible = True
        else:
            self._proxy.sliceIconVisible = False

        self._proxy.updateColorUI()

    @property
    def status(self):
        return self._proxy.sliceStatusEnum

    @status.setter
    def status(self, value):
        Logger.log("d", "Setting status: {} -> {}".format(self._proxy.sliceStatusEnum, value))
        if self._proxy.sliceStatusEnum is not value:
            self._proxy.sliceStatusEnum = value
        self.updateSliceWidget()

    def _onApplicationActivityChanged(self):
        printable_nodes_count = len(getPrintableNodes())

        sel_tool = SmartSliceSelectTool.getInstance()

    def _onJobFinished(self, job):
        if not self._jobs[self._current_job] or self._jobs[self._current_job].canceled:
            Logger.log("d", "Smart Slice Job was Cancelled")
            return

        if self._jobs[self._current_job].hasError():
            exc = self._jobs[self._current_job].getError()
            error = str(exc) if exc else "Unknown Error"
            self.cancelCurrentJob()
            Logger.logException("e", error)
            Message(
                title='Smart Slice Job Unexpectedly Failed',
                text=error,
                lifetime=0
            ).show()
            return

        self.propertyHandler._propertiesChanged.clear()
        self._proxy.shouldRaiseConfirmation = False

        if self._jobs[self._current_job].getResult():
            if len(self._jobs[self._current_job].getResult().analyses) > 0:
                if self._jobs[self._current_job].job_type == pywim.smartslice.job.JobType.optimization:
                    self.status = SmartSliceCloudStatus.Optimized
                    self.processAnalysisResult()
                else:
                    self.processAnalysisResult()
                    self.prepareOptimization()
                self.saveSmartSliceJob.emit()
            else:
                if self.status != SmartSliceCloudStatus.ReadyToVerify and self.status != SmartSliceCloudStatus.Errors:
                    self.status = SmartSliceCloudStatus.ReadyToVerify
                    results = self._jobs[self._current_job].getResult().feasibility_result['structural']
                    Message(
                        title="Smart Slice Error",
                        text="<p>Smart Slice cannot find a solution for the problem, "
                             "please check the setup for errors. </p>"
                             "<p></p>"
                             "<p>Alternatively, you may need to modify the geometry "
                             "and/or try a different material:</p>"
                             "<p></p>"
                             "<p> <u>Solid Print:</u> </p>"
                             "<p></p>"
                             "<p style = 'margin-left:50px;'> <i> Minimum Safety Factor: "
                             "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %.2f </i> </p>"
                             "<p></p>"
                             "<p style = 'margin-left:50px;'> <i> Maximum Displacement: "
                             "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %.2f </i> </p>" %
                             (results["min_safety_factor"], results["max_displacement"]),
                        lifetime=0,
                        dismissable=True
                    ).show()

    def processAnalysisResult(self, selectedRow=0):
        job = self._jobs[self._current_job]
        active_extruder = getNodeActiveExtruder(getPrintableNodes()[0])

        if job.job_type == pywim.smartslice.job.JobType.validation and active_extruder:
            resultData = ResultTableData.analysisToResultDict(0, job.getResult().analyses[0])
            self._proxy.updatePropertiesFromResults(resultData)

        elif job.job_type == pywim.smartslice.job.JobType.optimization and active_extruder:
            self._proxy.resultsTable.setResults(job.getResult().analyses, selectedRow)

    def updateStatus(self, show_warnings=False):
        if not self.smartSliceJobHandle:
            return

        job, self._proxy.errors = self.smartSliceJobHandle.checkJob(show_extruder_warnings=show_warnings)

        if len(self._proxy.errors) > 0 or job is None:
            self.status = SmartSliceCloudStatus.Errors
        elif self.status == SmartSliceCloudStatus.Errors or self.status == SmartSliceCloudStatus.Cancelling:
            self.status = SmartSliceCloudStatus.ReadyToVerify

        Application.getInstance().activityChanged.emit()

    def doVerification(self):
        self.status = SmartSliceCloudStatus.BusyValidating
        self.addJob(pywim.smartslice.job.JobType.validation)
        self._jobs[self._current_job].start()

    """
      prepareOptimization()
        Convenience function for updating the cloud status outside of Validation/Optimization Jobs
    """

    def prepareOptimization(self):
        self.status = self._proxy.optimizationStatus()
        self.updateSliceWidget()

    def doOptimization(self):
        if len(getModifierMeshes()) > 0:
            self.propertyHandler.askToRemoveModMesh()
        else:
            self.status = SmartSliceCloudStatus.BusyOptimizing
            self.addJob(pywim.smartslice.job.JobType.optimization)
            self._jobs[self._current_job].start()

    def _checkSubscription(self, subscription):
        if subscription.status == pywim.http.thor.Subscription.Status.inactive:
            if subscription.trial_end > datetime.datetime(1900, 1, 1):
                self._subscriptionMessages(self.SubscriptionTypes.trialExpired)
                return False
            else:
                self._subscriptionMessages(self.SubscriptionTypes.subscriptionExpired)
                return False

        return True

    def _subscriptionMessages(self, messageCode, prod=None):
        notification_message = Message(lifetime=0)

        if messageCode == self.SubscriptionTypes.trialExpired:
            notification_message.setText(
                i18n_catalog.i18nc("@info:status", "Your free trial has expired! Please subscribe to submit jobs.")
            )
        elif messageCode == self.SubscriptionTypes.subscriptionExpired:
            notification_message.setText(
                i18n_catalog.i18nc("@info:status", "Your subscription has expired! Please renew your subscription to submit jobs.")
            )

        notification_message.addAction(
            action_id="subscribe_link",
            name="<h3><b>Manage Subscription</b></h3>",
            icon="",
            description="Click here to subscribe!",
            button_style=Message.ActionButtonStyle.LINK
        )

        notification_message.actionTriggered.connect(self._openSubscriptionPage)
        notification_message.show()

    def _openSubscriptionPage(self, msg, action):
        if action in ("subscribe_link", "more_products_link"):
            QDesktopServices.openUrl(QUrl('%s/static/account.html' % self.extension.metadata.url))

    '''
      Primary Button Actions:
        * Validate
        * Optimize
        * Slice
    '''

    def onSliceButtonClicked(self):
        if self.status in SmartSliceCloudStatus.busy():
            self._jobs[self._current_job].cancel()
        else:
            self._subscription = self.api_connection.getSubscription()
            if self._subscription is not None:
                if self.status is SmartSliceCloudStatus.ReadyToVerify:
                    if self._checkSubscription(self._subscription):
                        self.doVerification()
                elif self.status in SmartSliceCloudStatus.optimizable():
                    if self._checkSubscription(self._subscription):
                        self.doOptimization()
                elif self.status is SmartSliceCloudStatus.Optimized:
                    Application.getInstance().getController().setActiveStage("PreviewStage")

    '''
      Secondary Button Actions:
        * Cancel  (Validating / Optimizing)
        * Preview
    '''

    def onSecondaryButtonClicked(self):
        if self.status in SmartSliceCloudStatus.busy():
            job_status = self._jobs[self._current_job].job_type
            self.cancelCurrentJob()
            if job_status == pywim.smartslice.job.JobType.optimization:
                self.prepareOptimization()
        else:
            Application.getInstance().getController().setActiveStage("PreviewStage")
示例#20
0
class MultiSlicePlugin(QObject, Extension):
    """
    A plugin for Ultimaker Cura that allows the user to load, slice, and export .gcode files
    for a series of model files based on an input directory and a regex file pattern to search for.
    See README.md for an example.

    Settings:
    :param self._file_pattern
        :GUI "File name pattern"
        :default r'.*.stl' (all .stl files)

        RegEx that defines the files that should be searched for. Only files matching this pattern
        are added to the list of files.

    :param self._input_path
        :GUI "Root directory"
        :default None

        The directory that will be walked when searching for files.

    :param self._output_path
        :GUI "Output directory"
        :default None

        The directory that .gcode files will be written to.

    :param self._follow_dirs
        :GUI "Follow directories
        :default False

        Whether or not to walk through directories found in self._input_path as well.

    :param self._follow_depth
        :GUI "Max depth"
        :default 0

        The maximum depth to walk when following directories. The root directory is treated as
        depth 0.

    :param self._preserve_dirs
        :GUI: "Preserve directories in output
        :default False

        Whether or not to produce the same folder structure in the output directory as found in
        the root directory.
    """
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        Extension.__init__(self)

        # add menu items in extensions menu
        self.setMenuName(catalog.i18nc("@item:inmenu", "Multi slicing"))
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Configure and run"),
                         self._show_popup)

        self._view = None  # type: Optional[QObject]

        # user options
        self._file_pattern = r'.*.stl'
        self._input_path = ''  # type: Union[Path, str]
        self._output_path = ''  # type: Union[Path, str]
        self._follow_dirs = False
        self._follow_depth = 0  # type: Optional[int]
        self._preserve_dirs = False

        self._files = []
        self._current_model = ''  # type: Union[Path, str]
        self._current_model_suffix = ''
        self._current_model_name = ''
        self._current_model_url = None  # type: Optional[QUrl]

        # gcode writer signal
        self._write_done = Signal()

        # event loop that allows us to wait for a signal
        self._loop = QEventLoop()

    # signal to handle output log messages
    log = pyqtSignal(str, name='log')

    def _log_msg(self, msg: str) -> None:
        """
        Emits a message to the logger signal
        """
        self.log.emit(msg)

    # signal to handle error messages
    error = pyqtSignal(str, name='error')

    def _send_error(self, msg: str) -> None:
        """
        Emits an error message to display in an error popup
        """
        self.error.emit(msg)

    # signal to send when processing is done
    processingDone = pyqtSignal(name='processingDone')

    def _signal_done(self) -> None:
        """
        Signals to the frontend that the current session is finished
        """
        self.processingDone.emit()

    def _create_view(self) -> None:
        """
        Create plugin view dialog
        """
        path = Path(PluginRegistry.getInstance().getPluginPath(
            'MultiSlice')) / 'MultiSliceView.qml'
        self._view = CuraApplication.getInstance().createQmlComponent(
            str(path), {'manager': self})

    def _show_popup(self) -> None:
        """
        Show plugin view dialog
        """
        if self._view is None:
            self._create_view()
            if self._view is None:
                Logger.log('e', 'Could not create QML')

        self._view.show()

    def _get_files(self, abs_paths: bool = False) -> List[Union[Path, str]]:
        """
        Recursively collect files from input dir relative to follow depth

        :param abs_paths: whether or not to collect absolute paths
        """
        files = []

        def _files(pattern: str, path: Path, depth: int):
            # skip if we exceeded recursion depth
            if depth > self._follow_depth:
                return

            try:
                for d in Path_(path).iterdir():

                    # if we reached a directory, do recursive call
                    if d.is_dir():
                        _files(pattern, d, depth + 1)
                    # if we reached a file, check if it matches file pattern and add to list if so
                    elif d.is_file() and re.match(pattern, d.name):
                        nonlocal files
                        files.append(d if abs_paths else d.name)

            except PermissionError:
                # if we can't read the current step, notify and skip
                self._log_msg(
                    'Could not access directory {0}, reason: permission denied. '
                    'Skipping.'.format(str(path)))
                return

        _files(self._file_pattern, self._input_path, 0)
        return files

    @pyqtProperty(list)
    def files_names(self) -> List[str]:
        """
        Retrieve names of all files matching settings
        """
        return self._get_files() or []

    @pyqtProperty(list)
    def files_paths(self) -> List[Path]:
        """
        Retrieve paths of all files matching settings
        """
        return self._get_files(abs_paths=True) or []

    @pyqtSlot(str)
    def set_input_path(self, path: str) -> None:
        """
        Set input path if valid
        """
        if path and os.path.isdir(path):
            self._input_path = Path(path)

    @pyqtSlot(str)
    def set_output_path(self, path: str) -> None:
        """
        Set output path if valid
        """
        if path and os.path.isdir(path):
            self._output_path = Path(path)

    @pyqtSlot(bool)
    def set_follow_dirs(self, follow: bool) -> None:
        """
        Set follow directories option
        """
        self._follow_dirs = follow

    @pyqtSlot(bool)
    def set_preserve_dirs(self, preserve: bool) -> None:
        """
        Set preserve directories option
        """
        self._preserve_dirs = preserve

    @pyqtSlot(str)
    def set_file_pattern(self, regex: str) -> None:
        """
        Set regex file pattern option if present, otherwise preserve default
        """
        if regex:
            self._file_pattern = regex

    @pyqtSlot(str)
    def set_follow_depth(self, depth: str) -> None:
        """
        Set follow depth option if present, otherwise preserve default
        """
        if depth:
            self._follow_depth = depth

    @pyqtProperty(bool)
    def validate_input(self) -> bool:
        """
        Try and validate applicable(bool options obviously don't need to be validated) options.
        Emit error and return false if any fail.
        """
        # file pattern should be valid regex
        try:
            re.compile(self._file_pattern)
        except re.error:
            self._send_error('Regex string \"{0}\" is not a valid regex. '
                             'Please try again.'.format(self._file_pattern))
            return False

        # input path should be a valid path
        if type(self._input_path) is str or not Path_(
                self._input_path).is_dir():
            self._send_error('Input path \"{0}\" is not a valid path. '
                             'Please try again.'.format(self._input_path))
            return False

        # output path should be a valid path
        if type(self._output_path) is str or not Path_(
                self._output_path).is_dir():
            self._send_error('Output path \"{0}\" is not a valid path. '
                             'Please try again.'.format(self._output_path))
            return False

        # follow depth should be an int
        try:
            self._follow_depth = int(self._follow_depth)
        except ValueError:
            self._send_error('Depth value \"{0}\" is not a valid integer. '
                             'Please try again.'.format(self._follow_depth))
            return False

        return True

    @pyqtSlot()
    def prepare_and_run(self) -> None:
        """
        Do initial setup for running and start
        """
        self._files = self.files_paths

        # don't proceed if no files are found
        if len(self._files) is 0:
            self._log_msg('Found 0 files, please try again')
            self._log_msg('-----')
            self._signal_done()
            return

        self._log_msg('Found {0} files'.format(len(self._files)))
        self._prepare_model()

        # slice when model is loaded
        CuraApplication.getInstance().fileCompleted.connect(self._slice)

        # write gcode to file when slicing is done
        Backend.Backend.backendStateChange.connect(self._write_gcode)

        # run next model when file is written
        self._write_done.connect(self._run_next)

        self._clear_models()

        self._run()

    def _prepare_next(self) -> None:
        """
        Prepare next model. If we don't have any models left to process, just set current to None.
        """
        # if we don't have any files left, set current model to none
        # current model is checked in function _run_next()
        if len(self._files) == 0:
            self._current_model = None
        else:
            self._log_msg('{0} file(s) to go'.format(len(self._files)))
            self._prepare_model()

    def _prepare_model(self) -> None:
        """
        Perform necessary actions and field assignments for a given model
        """
        self._current_model = self._files.pop()
        self._current_model_suffix = self._current_model.suffix
        self._current_model_name = self._current_model.name
        self._current_model_url = QUrl().fromLocalFile(str(
            self._current_model))

    def _run(self) -> None:
        """
        Run first iteration
        """
        self._load_model_and_slice()

    def _run_next(self) -> None:
        """
        Run subsequent iterations
        """
        self._log_msg('Clearing build plate and preparing next model')
        self._clear_models()
        self._prepare_next()

        if not self._current_model:
            self._log_msg('Found no more models. Done!')
            # reset signal connectors once all models are done
            self.__reset()
            self._signal_done()
            return

        self._load_model_and_slice()

    def _load_model_and_slice(self) -> None:
        """
        Read .stl file into Cura and wait for fileCompleted signal
        """
        self._log_msg('Loading model {0}'.format(self._current_model_name))
        CuraApplication.getInstance().readLocalFile(self._current_model_url)
        # wait for Cura to signal that it completed loading the file
        self._loop.exec()

    @staticmethod
    def _clear_models() -> None:
        """
        Clear all models on build plate
        """
        CuraApplication.getInstance().deleteAll()

    def _slice(self) -> None:
        """
        Begin slicing models on build plate and wait for backendStateChange to signal state 3,
        i.e. processing done
        """
        self._log_msg('Slicing...')
        CuraApplication.getInstance().backend.forceSlice()
        # wait for CuraEngine to signal that slicing is done
        self._loop.exec()

    def _write_gcode(self, state) -> None:
        """
        Write sliced model to file in output dir and emit signal once done
        """
        # state = 3 = process is done
        if state == 3:
            # ensure proper file suffix
            file_name = self._current_model_name.replace(
                self._current_model_suffix, '.gcode')

            # construct path relative to output directory using input path structure if we are
            # following directories
            # otherwise just dump it into the output directory
            if self._preserve_dirs:
                rel_path = self._current_model.relative_to(self._input_path)
                path = (self._output_path / rel_path).parent / file_name
                Path_(path.parent).mkdir(parents=True, exist_ok=True)
            else:
                path = self._output_path / file_name

            self._log_msg('Writing gcode to file {0}'.format(file_name))
            self._log_msg('Saving to directory: {0}'.format(str(path)))

            with Path_(path).open(mode='w') as stream:
                res = PluginRegistry.getInstance().getPluginObject(
                    "GCodeWriter").write(stream, [])

            # GCodeWriter notifies success state with bool
            if res:
                self._write_done.emit()

    def __reset(self) -> None:
        """
        Reset all signal connectors to allow running subsequent processes without several
        connector calls
        """
        CuraApplication.getInstance().fileCompleted.disconnect(self._slice)
        Backend.Backend.backendStateChange.disconnect(self._write_gcode)
        self._write_done.disconnect(self._run_next)

    @pyqtSlot()
    def stop_multi_slice(self) -> None:
        """
        Stop the session currently running
        """
        self._log_msg("Cancel signal emitted, stopping Multislice")
        self.__reset()
        self._loop.exit()

    @pyqtSlot(str)
    def trim(self, path: str) -> str:
        """
        Trims a file object from the frontend. What needs to be trimmed differes for
        Linux and Windows.
        """
        if platform.system() == "Windows":
            return path.replace('file:///', '')
        else:
            return path.replace('file://', '')
示例#21
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
示例#22
0
class LicensePresenter(QObject):

    def __init__(self, app: CuraApplication) -> None:
        super().__init__()
        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]
        self._license_model = LicenseModel()  # type: LicenseModel

        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, str]) -> None:
        path = os.path.join(plugin_path, self._compatibility_dialog_path)

        self._initState(packages)

        if self._dialog is None:

            context_properties = {
                "catalog": i18nCatalog("cura"),
                "licenseModel": self._license_model,
                "handler": self
            }
            self._dialog = self._app.createQmlComponent(path, context_properties)
        self._license_model.setPageCount(len(self._package_models))
        self._presentCurrentPackage()

    @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, str]) -> None:
        self._package_models = [
                {
                    "package_id" : package_id,
                    "package_path" : package_path,
                    "accepted" : None  #: None: no answer yet
                }
                for package_id, package_path in packages.items()
        ]

    def _presentCurrentPackage(self) -> None:
        package_model = self._package_models[self._current_package_idx]
        license_content = self._package_manager.getPackageLicense(package_model["package_path"])
        if license_content is None:
            # Implicitly accept when there is no license
            self.onLicenseAccepted()
            return

        self._license_model.setCurrentPageIdx(self._current_package_idx)
        self._license_model.setPackageName(package_model["package_id"])
        self._license_model.setLicenseText(license_content)
        if self._dialog:
            self._dialog.open()  # Does nothing if already open

    def _checkNextPage(self) -> None:
        if self._current_package_idx + 1 < len(self._package_models):
            self._current_package_idx += 1
            self._presentCurrentPackage()
        else:
            if self._dialog:
                self._dialog.close()
            self.licenseAnswers.emit(self._package_models)
示例#23
0
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()
示例#24
0
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 for 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.update(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",
            ))
        sync_message.addAction(
            "sync",
            name=self._i18n_catalog.i18nc("@action:button", "Sync"),
            icon="",
            description=
            "Sync your plugins and print profiles to Ultimaker Cura.",
            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)
示例#25
0
class LicensePresenter(QObject):

    def __init__(self, app: CuraApplication) -> None:
        super().__init__()
        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:
        path = os.path.join(plugin_path, self._compatibility_dialog_path)

        self._initState(packages)

        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()

    @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 CloudPackageChecker(QObject):
    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

    # 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._onLoginStateChanged()
        # check again whenever the login state changes
        self._application.getCuraAPI().account.loginStateChanged.connect(
            self._onLoginStateChanged)

    def _onLoginStateChanged(self) -> None:
        if self._application.getCuraAPI().account.isLoggedIn:
            self._getUserSubscribedPackages()
        elif self._message is not None:
            self._message.hide()
            self._message = None

    def _getUserSubscribedPackages(self) -> None:
        Logger.debug("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 subscribed packages from the Web Marketplace"
            )

    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.getUserInstalledPackages(
        )

        # 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(
            set(user_subscribed_packages).difference(user_installed_packages))
        if package_discrepancy:
            self._model.addDiscrepancies(package_discrepancy)
            self._model.initialize(self._package_manager,
                                   subscribed_packages_payload)
            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",
                "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 _onSyncButtonClicked(self, sync_message: Message,
                             sync_message_action: str) -> None:
        sync_message.hide()
        self.discrepancies.emit(self._model)
示例#27
0
class DownloadPresenter:

    DISK_WRITE_BUFFER_SIZE = 256 * 1024  # 256 KB

    def __init__(self, app: CuraApplication) -> None:
        # Emits (Dict[str, str], List[str]) # (success_items, error_items)
        # Dict{success_package_id, temp_file_path}
        # List[errored_package_id]
        self.done = Signal()

        self._app = app
        self._scope = UltimakerCloudScope(app)

        self._started = False
        self._progress_message = self._createProgressMessage()
        self._progress = {
        }  # type: Dict[str, Dict[str, Any]] # package_id, Dict
        self._error = []  # type: List[str] # package_id

    def download(self, model: SubscribedPackagesModel) -> None:
        if self._started:
            Logger.error("Download already started. Create a new %s instead",
                         self.__class__.__name__)
            return

        manager = HttpRequestManager.getInstance()
        for item in model.items:
            package_id = item["package_id"]

            def finishedCallback(reply: QNetworkReply, pid=package_id) -> None:
                self._onFinished(pid, reply)

            def progressCallback(rx: int, rt: int, pid=package_id) -> None:
                self._onProgress(pid, rx, rt)

            def errorCallback(reply: QNetworkReply,
                              error: QNetworkReply.NetworkError,
                              pid=package_id) -> None:
                self._onError(pid)

            request_data = manager.get(
                item["download_url"],
                callback=finishedCallback,
                download_progress_callback=progressCallback,
                error_callback=errorCallback,
                scope=self._scope)

            self._progress[package_id] = {
                "received": 0,
                "total":
                1,  # make sure this is not considered done yet. Also divByZero-safe
                "file_written": None,
                "request_data": request_data,
                "package_model": item
            }

        self._started = True
        self._progress_message.show()

    def abort(self) -> None:
        manager = HttpRequestManager.getInstance()
        for item in self._progress.values():
            manager.abortRequest(item["request_data"])

    # Aborts all current operations and returns a copy with the same settings such as app and scope
    def resetCopy(self) -> "DownloadPresenter":
        self.abort()
        self.done.disconnectAll()
        return DownloadPresenter(self._app)

    def _createProgressMessage(self) -> Message:
        return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."),
                       lifetime=0,
                       use_inactivity_timer=False,
                       progress=0.0,
                       title=i18n_catalog.i18nc(
                           "@info:title",
                           "Changes detected from your Ultimaker account",
                       ))

    def _onFinished(self, package_id: str, reply: QNetworkReply) -> None:
        self._progress[package_id]["received"] = self._progress[package_id][
            "total"]

        try:
            with tempfile.NamedTemporaryFile(mode="wb+",
                                             suffix=".curapackage",
                                             delete=False) as temp_file:
                bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
                while bytes_read:
                    temp_file.write(bytes_read)
                    bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
                    self._app.processEvents()
                self._progress[package_id]["file_written"] = temp_file.name
        except IOError as e:
            Logger.logException(
                "e", "Failed to write downloaded package to temp file", e)
            self._onError(package_id)
        temp_file.close()

        self._checkDone()

    def _onProgress(self, package_id: str, rx: int, rt: int) -> None:
        self._progress[package_id]["received"] = rx
        self._progress[package_id]["total"] = rt

        received = 0
        total = 0
        for item in self._progress.values():
            received += item["received"]
            total += item["total"]

        self._progress_message.setProgress(100.0 *
                                           (received / total))  # [0 .. 100] %

    def _onError(self, package_id: str) -> None:
        self._progress.pop(package_id)
        self._error.append(package_id)
        self._checkDone()

    def _checkDone(self) -> bool:
        for item in self._progress.values():
            if not item["file_written"]:
                return False

        success_items = {
            package_id: {
                "package_path": value["file_written"],
                "icon_url": value["package_model"]["icon_url"]
            }
            for package_id, value in self._progress.items()
        }
        error_items = [package_id for package_id in self._error]

        self._progress_message.hide()
        self.done.emit(success_items, error_items)
        return True
示例#28
0
class VariantNode(ContainerNode):
    def __init__(self, container_id: str, machine: "MachineNode") -> None:
        super().__init__(container_id)
        self.machine = machine
        self.materials = {}  # type: Dict[str, MaterialNode]  # Mapping material base files to their nodes.
        self.materialsChanged = Signal()

        container_registry = ContainerRegistry.getInstance()
        self.variant_name = container_registry.findContainersMetadata(id = container_id)[0]["name"]  # Store our own name so that we can filter more easily.
        container_registry.containerAdded.connect(self._materialAdded)
        container_registry.containerRemoved.connect(self._materialRemoved)
        self._loadAll()

    ##  (Re)loads all materials under this variant.
    @UM.FlameProfiler.profile
    def _loadAll(self) -> None:
        container_registry = ContainerRegistry.getInstance()

        if not self.machine.has_materials:
            self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
            return  # There should not be any materials loaded for this printer.

        # Find all the materials for this variant's name.
        else:  # Printer has its own material profiles. Look for material profiles with this printer's definition.
            base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
            printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = None)
            variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name)  # If empty_variant, this won't return anything.
            materials_per_base_file = {material["base_file"]: material for material in base_materials}
            materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials})  # Printer-specific profiles override global ones.
            materials_per_base_file.update({material["base_file"]: material for material in variant_specific_materials})  # Variant-specific profiles override all of those.
            materials = list(materials_per_base_file.values())

        # Filter materials based on the exclude_materials property.
        filtered_materials = [material for material in materials if material["id"] not in self.machine.exclude_materials]

        for material in filtered_materials:
            base_file = material["base_file"]
            if base_file not in self.materials:
                self.materials[base_file] = MaterialNode(material["id"], variant = self)
                self.materials[base_file].materialChanged.connect(self.materialsChanged)
        if not self.materials:
            self.materials["empty_material"] = MaterialNode("empty_material", variant = self)

    ##  Finds the preferred material for this printer with this nozzle in one of
    #   the extruders.
    #
    #   If the preferred material is not available, an arbitrary material is
    #   returned. If there is a configuration mistake (like a typo in the
    #   preferred material) this returns a random available material. If there
    #   are no available materials, this will return the empty material node.
    #   \param approximate_diameter The desired approximate diameter of the
    #   material.
    #   \return The node for the preferred material, or any arbitrary material
    #   if there is no match.
    def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
        for base_material, material_node in self.materials.items():
            if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
                return material_node
            
        # First fallback: Check if we should be checking for the 175 variant.
        if approximate_diameter == 2:
            preferred_material = self.machine.preferred_material + "_175"
            for base_material, material_node in self.materials.items():
                if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
                    return material_node
        
        # Second fallback: Choose any material with matching diameter.
        for material_node in self.materials.values():
            if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
                Logger.log("w", "Could not find preferred material %s, falling back to whatever works", self.machine.preferred_material)
                return material_node

        fallback = next(iter(self.materials.values()))  # Should only happen with empty material node.
        Logger.log("w", "Could not find preferred material {preferred_material} with diameter {diameter} for variant {variant_id}, falling back to {fallback}.".format(
            preferred_material = self.machine.preferred_material,
            diameter = approximate_diameter,
            variant_id = self.container_id,
            fallback = fallback.container_id
        ))
        return fallback

    ##  When a material gets added to the set of profiles, we need to update our
    #   tree here.
    @UM.FlameProfiler.profile
    def _materialAdded(self, container: ContainerInterface) -> None:
        if container.getMetaDataEntry("type") != "material":
            return  # Not interested.
        if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()):
            # CURA-6889
            # containerAdded and removed signals may be triggered in the next event cycle. If a container gets added
            # and removed in the same event cycle, in the next cycle, the connections should just ignore the signals.
            # The check here makes sure that the container in the signal still exists.
            Logger.log("d", "Got container added signal for container [%s] but it no longer exists, do nothing.",
                       container.getId())
            return
        if not self.machine.has_materials:
            return  # We won't add any materials.
        material_definition = container.getMetaDataEntry("definition")

        base_file = container.getMetaDataEntry("base_file")
        if base_file in self.machine.exclude_materials:
            return  # Material is forbidden for this printer.
        if base_file not in self.materials:  # Completely new base file. Always better than not having a file as long as it matches our set-up.
            if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
                return
            material_variant = container.getMetaDataEntry("variant_name")
            if material_variant is not None and material_variant != self.variant_name:
                return
        else:  # We already have this base profile. Replace the base profile if the new one is more specific.
            new_definition = container.getMetaDataEntry("definition")
            if new_definition == "fdmprinter":
                return  # Just as unspecific or worse.
            material_variant = container.getMetaDataEntry("variant_name")
            if new_definition != self.machine.container_id or material_variant != self.variant_name:
                return  # Doesn't match this set-up.
            original_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.materials[base_file].container_id)[0]
            if "variant_name" in original_metadata or material_variant is None:
                return  # Original was already specific or just as unspecific as the new one.

        if "empty_material" in self.materials:
            del self.materials["empty_material"]
        self.materials[base_file] = MaterialNode(container.getId(), variant = self)
        self.materials[base_file].materialChanged.connect(self.materialsChanged)
        self.materialsChanged.emit(self.materials[base_file])

    @UM.FlameProfiler.profile
    def _materialRemoved(self, container: ContainerInterface) -> None:
        if container.getMetaDataEntry("type") != "material":
            return  # Only interested in materials.
        base_file = container.getMetaDataEntry("base_file")
        if base_file not in self.materials:
            return  # We don't track this material anyway. No need to remove it.

        original_node = self.materials[base_file]
        del self.materials[base_file]
        self.materialsChanged.emit(original_node)

        # Now a different material from the same base file may have been hidden because it was not as specific as the one we deleted.
        # Search for any submaterials from that base file that are still left.
        materials_same_base_file = ContainerRegistry.getInstance().findContainersMetadata(base_file = base_file)
        if materials_same_base_file:
            most_specific_submaterial = None
            for submaterial in materials_same_base_file:
                if submaterial["definition"] == self.machine.container_id:
                    if submaterial.get("variant_name", "empty") == self.variant_name:
                        most_specific_submaterial = submaterial
                        break  # most specific match possible
                    if submaterial.get("variant_name", "empty") == "empty":
                        most_specific_submaterial = submaterial

            if most_specific_submaterial is None:
                Logger.log("w", "Material %s removed, but no suitable replacement found", base_file)
            else:
                Logger.log("i", "Material %s (%s) overridden by %s", base_file, self.variant_name, most_specific_submaterial.get("id"))
                self.materials[base_file] = MaterialNode(most_specific_submaterial["id"], variant = self)
                self.materialsChanged.emit(self.materials[base_file])

        if not self.materials:  # The last available material just got deleted and there is nothing with the same base file to replace it.
            self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
            self.materialsChanged.emit(self.materials["empty_material"])
示例#29
0
class ResultTableData(QAbstractListModel):

    selectedRowChanged = pyqtSignal()
    sortColumnChanged = pyqtSignal()
    sortOrderChanged = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self._results = []  # List[pywim.smartslice.result.Analysis]
        self._resultsDict = [
        ]  # List[Dict[]] --> A list of dictionary items for sorting

        self._selectedRow = 0
        self._sortColumn = 0
        self._sortOrder = Qt.AscendingOrder

        self.updateDisplaySignal = Signal(
        )  # Tells the owner of the table when to  update the display (like when a row is clicked)
        self.resultsUpdated = Signal()

    def setResults(self,
                   results: List[pywim.smartslice.result.Analysis],
                   requested_result=0):

        self.beginRemoveRows(QModelIndex(), 0, len(self._resultsDict) - 1)
        self._results.clear()
        self._resultsDict.clear()
        self.endRemoveRows()

        self._results = results
        self.selectedRow = -1

        self.sortColumn = 0
        self.sortOrder = Qt.AscendingOrder

        rank = 0
        row = 0
        for result in self._results:
            rank += 1
            self._resultsDict.append(self.analysisToResultDict(rank, result))
            if rank - 1 == requested_result:
                row = rank - 1

        self.beginInsertRows(QModelIndex(), 0, len(self._resultsDict) - 1)
        self.endInsertRows()

        self.selectedRow = row
        self.sortByColumn(0, Qt.AscendingOrder)
        self.updateDisplaySignal.emit(self._resultsDict[row])
        self.resultsUpdated.emit()

    def roleNames(self):
        return ResultsTableHeader.rolesAsBytes()

    @pyqtProperty(int, notify=selectedRowChanged)
    def selectedRow(self):
        return self._selectedRow

    @selectedRow.setter
    def selectedRow(self, value):
        if self._selectedRow is not value:
            self._selectedRow = value
            self.selectedRowChanged.emit()

    @pyqtProperty(int, notify=sortColumnChanged)
    def sortColumn(self):
        return self._sortColumn

    @sortColumn.setter
    def sortColumn(self, value):
        if self._sortColumn is not value:
            self._sortColumn = value
            self.sortColumnChanged.emit()

    @pyqtProperty(int, notify=sortOrderChanged)
    def sortOrder(self):
        return self._sortOrder

    @sortOrder.setter
    def sortOrder(self, value):
        if self._sortOrder is not value:
            self._sortOrder = value
            self.sortOrderChanged.emit()

    @property
    def analyses(self):
        return self._results

    @pyqtSlot(QObject, result=int)
    def rowCount(self, parent=None) -> int:
        return len(self._resultsDict)

    def getSelectedResultId(self):
        if self._selectedRow >= 0 and self._selectedRow < len(
                self._resultsDict):
            return self._resultsDict[self._selectedRow][
                ResultsTableHeader.Rank.value] - 1
        return 0

    def data(self, index, role):
        if len(self._resultsDict) > index.row():
            if len(ResultsTableHeader.rolesAsBytes()) > role:
                value = self._resultsDict[index.row()][role]

                if role == ResultsTableHeader.Time.value:
                    return Duration(value).getDisplayString()

                elif role == ResultsTableHeader.Mass.value:
                    return "{}g".format(round(value, 0))

                elif role == ResultsTableHeader.Displacement.value:
                    return "{} mm".format(round(value, 2))

                elif role == ResultsTableHeader.Strength.value:
                    return round(value, 1)

                else:
                    return value

        return None

    @pyqtSlot(int)
    def sortByColumn(self, column=0, order=None):

        if column >= ResultsTableHeader.numRoles():
            return

        if order is None:
            if column != self.sortColumn:
                self.sortOrder = Qt.AscendingOrder
            else:
                if self.sortOrder == Qt.AscendingOrder:
                    self.sortOrder = Qt.DescendingOrder
                else:
                    self.sortOrder = Qt.AscendingOrder
        else:
            self.sortOrder = order

        self.sortColumn = column

        descending = True if self.sortOrder is Qt.DescendingOrder else False

        rank = self._resultsDict[self._selectedRow][
            ResultsTableHeader.Rank.value]

        self._resultsDict.sort(reverse=descending,
                               key=lambda result: result[column])

        self.beginRemoveRows(QModelIndex(), 0, len(self._resultsDict) - 1)
        self.endRemoveRows()

        self.beginInsertRows(QModelIndex(), 0, len(self._resultsDict) - 1)
        self.endInsertRows()

        i = 0
        for result in self._resultsDict:
            if result[ResultsTableHeader.Rank.value] == rank:
                self.selectedRow = i
            i += 1

    @pyqtSlot(int)
    def rowClicked(self, row):
        if row < len(self._resultsDict):
            self.selectedRow = row
            QApplication.setOverrideCursor(Qt.WaitCursor)
            self.updateDisplaySignal.emit(self._resultsDict[row])
            QApplication.restoreOverrideCursor()

            # This is needed to stop the cursor from rotating indefinitely in the table area
            QApplication.setOverrideCursor(Qt.ArrowCursor)
            QApplication.restoreOverrideCursor()

    @pyqtSlot()
    def previewClicked(self):
        Application.getInstance().getController().setActiveStage(
            "PreviewStage")

    @classmethod
    def analysisToResultDict(self, rank,
                             result: pywim.smartslice.result.Analysis):
        material_data = self.calculateAdditionalMaterialInfo(result)

        return {
            ResultsTableHeader.Rank.value: rank,
            ResultsTableHeader.Time.value: result.print_time,
            ResultsTableHeader.Strength.value:
            result.structural.min_safety_factor,
            ResultsTableHeader.Displacement.value:
            result.structural.max_displacement,
            ResultsTableHeader.Length.value: material_data[0][0],
            ResultsTableHeader.Mass.value: material_data[1][0],
            ResultsTableHeader.Cost.value: material_data[2][0]
        }

    @classmethod
    def calculateAdditionalMaterialInfo(
            self, result: pywim.smartslice.result.Analysis):

        _material_volume = [result.extruders[0].material_volume]

        application = Application.getInstance()

        global_stack = application.getGlobalContainerStack()
        if global_stack is None:
            return

        _material_lengths = []
        _material_weights = []
        _material_costs = []
        _material_names = []

        material_preference_values = json.loads(
            application.getPreferences().getValue("cura/material_settings"))

        Logger.log(
            "d",
            "global_stack.extruderList: {}".format(global_stack.extruderList))

        for extruder_stack in global_stack.extruderList:
            position = extruder_stack.position
            if type(position) is not int:
                position = int(position)
            if position >= len(_material_volume):
                continue
            amount = _material_volume[position]
            # Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
            # list comprehension filtering to solve this for us.
            density = extruder_stack.getMetaDataEntry("properties",
                                                      {}).get("density", 0)
            material = extruder_stack.material
            radius = extruder_stack.getProperty("material_diameter",
                                                "value") / 2

            weight = float(amount) * float(density) / 1000
            cost = 0.

            material_guid = material.getMetaDataEntry("GUID")
            material_name = material.getName()

            if material_guid in material_preference_values:
                material_values = material_preference_values[material_guid]

                if material_values and "spool_weight" in material_values:
                    weight_per_spool = float(material_values["spool_weight"])
                else:
                    weight_per_spool = float(
                        extruder_stack.getMetaDataEntry("properties",
                                                        {}).get("weight", 0))

                cost_per_spool = float(
                    material_values["spool_cost"] if material_values
                    and "spool_cost" in material_values else 0)

                if weight_per_spool != 0:
                    cost = cost_per_spool * weight / weight_per_spool
                else:
                    cost = 0

            # Material amount is sent as an amount of mm^3, so calculate length from that
            if radius != 0:
                length = round((amount / (math.pi * radius**2)) / 1000, 2)
            else:
                length = 0

            _material_weights.append(weight)
            _material_lengths.append(length)
            _material_costs.append(cost)
            _material_names.append(material_name)

        return _material_lengths, _material_weights, _material_costs, _material_names
示例#30
0
class MaterialNode(ContainerNode):
    def __init__(self, container_id: str, variant: "VariantNode") -> None:
        super().__init__(container_id)
        self.variant = variant
        self.qualities = {
        }  # type: Dict[str, QualityNode] # Mapping container IDs to quality profiles.
        self.materialChanged = Signal(
        )  # Triggered when the material is removed or its metadata is updated.

        container_registry = ContainerRegistry.getInstance()
        my_metadata = container_registry.findContainersMetadata(
            id=container_id)[0]
        self.base_file = my_metadata["base_file"]
        self.material_type = my_metadata["material"]
        self.guid = my_metadata["GUID"]
        self._loadAll()
        container_registry.containerRemoved.connect(self._onRemoved)
        container_registry.containerMetaDataChanged.connect(
            self._onMetadataChanged)

    ##  Finds the preferred quality for this printer with this material and this
    #   variant loaded.
    #
    #   If the preferred quality is not available, an arbitrary quality is
    #   returned. If there is a configuration mistake (like a typo in the
    #   preferred quality) this returns a random available quality. If there are
    #   no available qualities, this will return the empty quality node.
    #   \return The node for the preferred quality, or any arbitrary quality if
    #   there is no match.
    def preferredQuality(self) -> QualityNode:
        for quality_id, quality_node in self.qualities.items():
            if self.variant.machine.preferred_quality_type == quality_node.quality_type:
                return quality_node
        fallback = next(iter(self.qualities.values())
                        )  # Should only happen with empty quality node.
        Logger.log(
            "w",
            "Could not find preferred quality type {preferred_quality_type} for material {material_id} and variant {variant_id}, falling back to {fallback}."
            .format(preferred_quality_type=self.variant.machine.
                    preferred_quality_type,
                    material_id=self.container_id,
                    variant_id=self.variant.container_id,
                    fallback=fallback.container_id))
        return fallback

    @UM.FlameProfiler.profile
    def _loadAll(self) -> None:
        container_registry = ContainerRegistry.getInstance()
        # Find all quality profiles that fit on this material.
        if not self.variant.machine.has_machine_quality:  # Need to find the global qualities.
            qualities = container_registry.findInstanceContainersMetadata(
                type="quality", definition="fdmprinter")
        elif not self.variant.machine.has_materials:
            qualities = container_registry.findInstanceContainersMetadata(
                type="quality",
                definition=self.variant.machine.quality_definition)
        else:
            if self.variant.machine.has_variants:
                # Need to find the qualities that specify a material profile with the same material type.
                qualities = container_registry.findInstanceContainersMetadata(
                    type="quality",
                    definition=self.variant.machine.quality_definition,
                    variant=self.variant.variant_name,
                    material=self.container_id
                )  # First try by exact material ID.
            else:
                qualities = container_registry.findInstanceContainersMetadata(
                    type="quality",
                    definition=self.variant.machine.quality_definition,
                    material=self.container_id)
            if not qualities:
                my_material_type = self.material_type
                if self.variant.machine.has_variants:
                    qualities_any_material = container_registry.findInstanceContainersMetadata(
                        type="quality",
                        definition=self.variant.machine.quality_definition,
                        variant=self.variant.variant_name)
                else:
                    qualities_any_material = container_registry.findInstanceContainersMetadata(
                        type="quality",
                        definition=self.variant.machine.quality_definition)
                for material_metadata in container_registry.findInstanceContainersMetadata(
                        type="material", material=my_material_type):
                    qualities.extend((
                        quality for quality in qualities_any_material
                        if quality.get("material") == material_metadata["id"]))

                if not qualities:  # No quality profiles found. Go by GUID then.
                    my_guid = self.guid
                    for material_metadata in container_registry.findInstanceContainersMetadata(
                            type="material", guid=my_guid):
                        qualities.extend((
                            quality for quality in qualities_any_material
                            if quality["material"] == material_metadata["id"]))

                if not qualities:
                    # There are still some machines that should use global profiles in the extruder, so do that now.
                    # These are mostly older machines that haven't received updates (so single extruder machines without specific qualities
                    # but that do have materials and profiles specific to that machine)
                    qualities.extend([
                        quality for quality in qualities_any_material
                        if quality.get("global_quality", "False") != "False"
                    ])

        for quality in qualities:
            quality_id = quality["id"]
            if quality_id not in self.qualities:
                self.qualities[quality_id] = QualityNode(quality_id,
                                                         parent=self)
        if not self.qualities:
            self.qualities["empty_quality"] = QualityNode("empty_quality",
                                                          parent=self)

    ##  Triggered when any container is removed, but only handles it when the
    #   container is removed that this node represents.
    #   \param container The container that was allegedly removed.
    def _onRemoved(self, container: ContainerInterface) -> None:
        if container.getId() == self.container_id:
            # Remove myself from my parent.
            if self.base_file in self.variant.materials:
                del self.variant.materials[self.base_file]
                if not self.variant.materials:
                    self.variant.materials["empty_material"] = MaterialNode(
                        "empty_material", variant=self.variant)
            self.materialChanged.emit(self)

    ##  Triggered when any metadata changed in any container, but only handles
    #   it when the metadata of this node is changed.
    #   \param container The container whose metadata changed.
    #   \param kwargs Key-word arguments provided when changing the metadata.
    #   These are ignored. As far as I know they are never provided to this
    #   call.
    def _onMetadataChanged(self, container: ContainerInterface,
                           **kwargs: Any) -> None:
        if container.getId() != self.container_id:
            return

        new_metadata = container.getMetaData()
        old_base_file = self.base_file
        if new_metadata["base_file"] != old_base_file:
            self.base_file = new_metadata["base_file"]
            if old_base_file in self.variant.materials:  # Move in parent node.
                del self.variant.materials[old_base_file]
            self.variant.materials[self.base_file] = self

        old_material_type = self.material_type
        self.material_type = new_metadata["material"]
        old_guid = self.guid
        self.guid = new_metadata["GUID"]
        if self.base_file != old_base_file or self.material_type != old_material_type or self.guid != old_guid:  # List of quality profiles could've changed.
            self.qualities = {}
            self._loadAll()  # Re-load the quality profiles for this node.
        self.materialChanged.emit(self)
示例#31
0
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()