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)
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)
def test_connectWhilePostponed(): test = SignalReceiver() signal = Signal(type=Signal.Direct) with postponeSignals(signal): signal.connect(test.slot) # This won't do anything, as we're postponing at the moment! signal.emit() assert test.getEmitCount() == 0 # The connection was never made, so we should get 0
def test_signal(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) signal.emit() assert test.getEmitCount() == 1
def test_signal(): test = SignalReceiver() signal = Signal(type = Signal.Direct) signal.connect(test.slot) signal.emit() assert test.getEmitCount() == 1
def test_disconnectWhilePostponed(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) with postponeSignals(signal): signal.disconnect(test.slot) # This won't do anything, as we're postponing at the moment! signal.disconnectAll() # Same holds true for the disconnect all signal.emit() assert test.getEmitCount() == 1 # Despite attempting to disconnect, this didn't happen because of the postpone
def test_signalWithFlameProfiler(): with patch("UM.Signal._recordSignalNames", MagicMock(return_value = True)): FlameProfiler.record_profile = True test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) signal.emit() assert test.getEmitCount() == 1 FlameProfiler.record_profile = False
def test_doubleSignalWithFlameProfiler(): FlameProfiler.record_profile = True test = SignalReceiver() signal = Signal(type=Signal.Direct) signal2 = Signal(type=Signal.Direct) signal.connect(test.slot) signal2.connect(signal) signal2.emit() assert test.getEmitCount() == 1 FlameProfiler.record_profile = False
def test_doubleSignalWithFlameProfiler(): FlameProfiler.record_profile = True test = SignalReceiver() signal = Signal(type=Signal.Direct) signal2 = Signal(type=Signal.Direct) signal.connect(test.slot) signal2.connect(signal) signal2.emit() assert test.getEmitCount() == 1 FlameProfiler.record_profile = False
def test_signalWithFlameProfiler(): with patch("UM.Signal._recordSignalNames", MagicMock(return_value=True)): FlameProfiler.record_profile = True test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) signal.emit() assert test.getEmitCount() == 1 FlameProfiler.record_profile = False
def test_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
def test_connectWhilePostponed(): test = SignalReceiver() signal = Signal(type=Signal.Direct) with postponeSignals(signal): signal.connect( test.slot ) # This won't do anything, as we're postponing at the moment! signal.emit() assert test.getEmitCount( ) == 0 # The connection was never made, so we should get 0
def test_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
def test_disconnectWhilePostponed(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) with postponeSignals(signal): signal.disconnect( test.slot ) # This won't do anything, as we're postponing at the moment! signal.disconnectAll() # Same holds true for the disconnect all signal.emit() assert test.getEmitCount( ) == 1 # Despite attempting to disconnect, this didn't happen because of the postpone
def test_postponeEmitCompressPerParameterValue(): test = SignalReceiver() signal = Signal(type=Signal.Direct) signal.connect(test.slot) with postponeSignals(signal, compress=CompressTechnique.CompressPerParameterValue): signal.emit("ZOMG") assert test.getEmitCount() == 0 # as long as we're in this context, nothing should happen! signal.emit("ZOMG") assert test.getEmitCount() == 0 signal.emit("BEEP") # We got 3 signal emits, but 2 of them were the same, so we end up with 2 unique emits. assert test.getEmitCount() == 2
def 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
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)
def test_connectSelf(): signal = Signal(type=Signal.Direct) signal.connect(signal) signal.emit( ) # If they are connected, this crashes with a max recursion depth error
class 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... (<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: " " %.2f </i> </p>" "<p></p>" "<p style = 'margin-left:50px;'> <i> Maximum Displacement: " " %.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")
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://', '')
def test_connectSelf(): signal = Signal(type=Signal.Direct) signal.connect(signal) signal.emit() # If they are connected, this crashes with a max recursion depth error
class 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)
class NetworkManagerMock: # An enumeration of the supported operations and their code for the network access manager. _OPERATIONS = { "GET": QNetworkAccessManager.GetOperation, "POST": QNetworkAccessManager.PostOperation, "PUT": QNetworkAccessManager.PutOperation, "DELETE": QNetworkAccessManager.DeleteOperation, "HEAD": QNetworkAccessManager.HeadOperation, } # type: Dict[str, int] ## Initializes the network manager mock. def __init__(self) -> None: # A dict with the prepared replies, using the format {(http_method, url): reply} self.replies = {} # type: Dict[Tuple[str, str], MagicMock] self.request_bodies = {} # type: Dict[Tuple[str, str], bytes] # Signals used in the network manager. self.finished = Signal() self.authenticationRequired = Signal() ## Mock implementation of the get, post, put, delete and head methods from the network manager. # Since the methods are very simple and the same it didn't make sense to repeat the code. # \param method: The method being called. # \return The mocked function, if the method name is known. Defaults to the standard getattr function. def __getattr__(self, method: str) -> Any: ## This mock implementation will simply return the reply from the prepared ones. # it raises a KeyError if requests are done without being prepared. def doRequest(request: QNetworkRequest, body: Optional[bytes] = None, *_): key = method.upper(), request.url().toString() if body: self.request_bodies[key] = body return self.replies[key] operation = self._OPERATIONS.get(method.upper()) if operation: return doRequest # the attribute is not one of the implemented methods, default to the standard implementation. return getattr(super(), method) ## Prepares a server reply for the given parameters. # \param method: The HTTP method. # \param url: The URL being requested. # \param status_code: The HTTP status code for the response. # \param response: The response body from the server (generally json-encoded). def prepareReply(self, method: str, url: str, status_code: int, response: Union[bytes, dict]) -> None: reply_mock = MagicMock() reply_mock.url().toString.return_value = url reply_mock.operation.return_value = self._OPERATIONS[method] reply_mock.attribute.return_value = status_code reply_mock.finished = FakeSignal() reply_mock.isFinished.return_value = False reply_mock.readAll.return_value = response if isinstance( response, bytes) else json.dumps(response).encode() self.replies[method, url] = reply_mock Logger.log("i", "Prepared mock {}-response to {} {}", status_code, method, url) ## Gets the request that was sent to the network manager for the given method and URL. # \param method: The HTTP method. # \param url: The URL. def getRequestBody(self, method: str, url: str) -> Optional[bytes]: return self.request_bodies.get((method.upper(), url)) ## Emits the signal that the reply is ready to all prepared replies. def flushReplies(self) -> None: for key, reply in self.replies.items(): Logger.log("i", "Flushing reply to {} {}", *key) reply.isFinished.return_value = True reply.finished.emit() self.finished.emit(reply) self.reset() ## Deletes all prepared replies def reset(self) -> None: self.replies.clear()
class 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)
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)
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
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"])
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
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)
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()