diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 07879f33cf..c1dff4a403 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1727,3 +1727,7 @@ class CuraApplication(QtApplication): node = node.getParent() Selection.add(node) + + @pyqtSlot() + def showMoreInformationDialogForAnonymousDataCollection(self): + self._plugin_registry.getPluginObject("SliceInfoPlugin").showMoreInfoDialog() diff --git a/plugins/SliceInfoPlugin/MoreInfoWindow.qml b/plugins/SliceInfoPlugin/MoreInfoWindow.qml new file mode 100644 index 0000000000..985ebe94a2 --- /dev/null +++ b/plugins/SliceInfoPlugin/MoreInfoWindow.qml @@ -0,0 +1,151 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Window 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.3 as UM +import Cura 1.0 as Cura + + +UM.Dialog +{ + id: baseDialog + title: catalog.i18nc("@title:window", "More information on anonymous data collection") + visible: false + + minimumWidth: 500 * screenScaleFactor + minimumHeight: 400 * screenScaleFactor + width: minimumWidth + height: minimumHeight + + property bool allowSendData: true // for saving the user's choice + + onAccepted: manager.setSendSliceInfo(allowSendData) + + onVisibilityChanged: + { + if (visible) + { + baseDialog.allowSendData = UM.Preferences.getValue("info/send_slice_info"); + if (baseDialog.allowSendData) + { + allowSendButton.checked = true; + } + else + { + dontSendButton.checked = true; + } + } + } + + Item + { + id: textRow + anchors + { + top: parent.top + bottom: radioButtonsRow.top + bottomMargin: UM.Theme.getSize("default_margin").height + left: parent.left + right: parent.right + } + + Label + { + id: headerText + anchors + { + top: parent.top + left: parent.left + right: parent.right + } + + text: catalog.i18nc("@text:window", "Cura sends anonymous data to Ultimaker in order to improve the print quality and user experience. Below is an example of all the data that is sent.") + wrapMode: Text.WordWrap + } + + TextArea + { + id: exampleData + anchors + { + top: headerText.bottom + topMargin: UM.Theme.getSize("default_margin").height + bottom: parent.bottom + bottomMargin: UM.Theme.getSize("default_margin").height + left: parent.left + right: parent.right + } + + text: manager.getExampleData() + readOnly: true + textFormat: TextEdit.PlainText + } + } + + Column + { + id: radioButtonsRow + width: parent.width + anchors.bottom: buttonRow.top + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + + ExclusiveGroup { id: group } + + RadioButton + { + id: dontSendButton + text: catalog.i18nc("@text:window", "I don't want to send these data") + exclusiveGroup: group + onClicked: + { + baseDialog.allowSendData = !checked; + } + } + RadioButton + { + id: allowSendButton + text: catalog.i18nc("@text:window", "Allow sending these data to Ultimaker and help us improve Cura") + exclusiveGroup: group + onClicked: + { + baseDialog.allowSendData = checked; + } + } + } + + Item + { + id: buttonRow + anchors.bottom: parent.bottom + width: parent.width + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + + UM.I18nCatalog { id: catalog; name: "cura" } + + Button + { + anchors.right: parent.right + text: catalog.i18nc("@action:button", "OK") + onClicked: + { + baseDialog.accepted() + baseDialog.hide() + } + } + + Button + { + anchors.left: parent.left + text: catalog.i18nc("@action:button", "Cancel") + onClicked: + { + baseDialog.rejected() + baseDialog.hide() + } + } + } +} diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index d618f63b53..82e07da464 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -1,8 +1,12 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from cura.CuraApplication import CuraApplication -from cura.Settings.ExtruderManager import ExtruderManager +import json +import os +import platform +import time + +from PyQt5.QtCore import pyqtSlot, QObject from UM.Extension import Extension from UM.Application import Application @@ -11,18 +15,11 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Message import Message from UM.i18n import i18nCatalog from UM.Logger import Logger - -import time - +from UM.PluginRegistry import PluginRegistry from UM.Qt.Duration import DurationFormat from .SliceInfoJob import SliceInfoJob -import platform -import math -import urllib.request -import urllib.parse -import json catalog = i18nCatalog("cura") @@ -30,15 +27,19 @@ catalog = i18nCatalog("cura") ## This Extension runs in the background and sends several bits of information to the Ultimaker servers. # The data is only sent when the user in question gave permission to do so. All data is anonymous and # no model files are being sent (Just a SHA256 hash of the model). -class SliceInfo(Extension): +class SliceInfo(QObject, Extension): info_url = "https://stats.ultimaker.com/api/cura" - def __init__(self): - super().__init__() + def __init__(self, parent = None): + QObject.__init__(self, parent) + Extension.__init__(self) Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted) Preferences.getInstance().addPreference("info/send_slice_info", True) Preferences.getInstance().addPreference("info/asked_send_slice_info", False) + self._more_info_dialog = None + self._example_data_content = None + if not Preferences.getInstance().getValue("info/asked_send_slice_info"): self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."), lifetime = 0, @@ -47,32 +48,64 @@ class SliceInfo(Extension): self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None, description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing.")) - self.send_slice_info_message.addAction("Disable", name = catalog.i18nc("@action:button", "Disable"), icon = None, - description = catalog.i18nc("@action:tooltip", "Don't allow Cura to send anonymized usage statistics. You can enable it again in the preferences."), button_style = Message.ActionButtonStyle.LINK) + self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None, + description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK) self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered) self.send_slice_info_message.show() + Application.getInstance().initializationFinished.connect(self._onAppInitialized) + + def _onAppInitialized(self): + if self._more_info_dialog is None: + self._more_info_dialog = self._createDialog("MoreInfoWindow.qml") + ## Perform action based on user input. # Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it. def messageActionTriggered(self, message_id, action_id): Preferences.getInstance().setValue("info/asked_send_slice_info", True) - if action_id == "Disable": - Preferences.getInstance().addPreference("info/send_slice_info", False) + if action_id == "MoreInfo": + self.showMoreInfoDialog() self.send_slice_info_message.hide() + def showMoreInfoDialog(self): + if self._more_info_dialog is None: + self._more_info_dialog = self._createDialog("MoreInfoWindow.qml") + self._more_info_dialog.open() + + def _createDialog(self, qml_name): + Logger.log("d", "Creating dialog [%s]", qml_name) + file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name) + dialog = Application.getInstance().createQmlComponent(file_path, {"manager": self}) + return dialog + + @pyqtSlot(result = str) + def getExampleData(self) -> str: + if self._example_data_content is None: + file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "example_data.json") + with open(file_path, "r", encoding = "utf-8") as f: + self._example_data_content = f.read() + return self._example_data_content + + @pyqtSlot(bool) + def setSendSliceInfo(self, enabled: bool): + Preferences.getInstance().setValue("info/send_slice_info", enabled) + def _onWriteStarted(self, output_device): try: if not Preferences.getInstance().getValue("info/send_slice_info"): Logger.log("d", "'info/send_slice_info' is turned off.") return # Do nothing, user does not want to send data - global_container_stack = Application.getInstance().getGlobalContainerStack() - print_information = Application.getInstance().getPrintInformation() + application = Application.getInstance() + machine_manager = application.getMachineManager() + print_information = application.getPrintInformation() + + global_stack = machine_manager.activeMachine data = dict() # The data that we're going to submit. data["time_stamp"] = time.time() data["schema_version"] = 0 - data["cura_version"] = Application.getInstance().getVersion() + data["cura_version"] = application.getVersion() active_mode = Preferences.getInstance().getValue("cura/active_mode") if active_mode == 0: @@ -80,7 +113,7 @@ class SliceInfo(Extension): else: data["active_mode"] = "custom" - definition_changes = global_container_stack.definitionChanges + definition_changes = global_stack.definitionChanges machine_settings_changed_by_user = False if definition_changes.getId() != "empty": # Now a definition_changes container will always be created for a stack, @@ -92,16 +125,17 @@ class SliceInfo(Extension): data["language"] = Preferences.getInstance().getValue("general/language") data["os"] = {"type": platform.system(), "version": platform.version()} - data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")} + data["active_machine"] = {"definition_id": global_stack.definition.getId(), + "manufacturer": global_stack.definition.getMetaDataEntry("manufacturer", "")} # add extruder specific data to slice info data["extruders"] = [] - extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId())) + extruders = list(global_stack.extruders.values()) extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position")) for extruder in extruders: extruder_dict = dict() - extruder_dict["active"] = ExtruderManager.getInstance().getActiveExtruderStack() == extruder + extruder_dict["active"] = machine_manager.activeStack == extruder extruder_dict["material"] = {"GUID": extruder.material.getMetaData().get("GUID", ""), "type": extruder.material.getMetaData().get("material", ""), "brand": extruder.material.getMetaData().get("brand", "") @@ -123,11 +157,11 @@ class SliceInfo(Extension): extruder_dict["extruder_settings"] = extruder_settings data["extruders"].append(extruder_dict) - data["quality_profile"] = global_container_stack.quality.getMetaData().get("quality_type") + data["quality_profile"] = global_stack.quality.getMetaData().get("quality_type") data["models"] = [] # Listing all files placed on the build plate - for node in DepthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()): + for node in DepthFirstIterator(application.getController().getScene().getRoot()): if node.callDecoration("isSliceable"): model = dict() model["hash"] = node.getMeshData().getHash() @@ -173,28 +207,28 @@ class SliceInfo(Extension): "total": int(print_information.currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))} print_settings = dict() - print_settings["layer_height"] = global_container_stack.getProperty("layer_height", "value") + print_settings["layer_height"] = global_stack.getProperty("layer_height", "value") # Support settings - print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value") - print_settings["support_extruder_nr"] = int(global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")) + print_settings["support_enabled"] = global_stack.getProperty("support_enable", "value") + print_settings["support_extruder_nr"] = int(global_stack.getExtruderPositionValueWithDefault("support_extruder_nr")) # Platform adhesion settings - print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value") + print_settings["adhesion_type"] = global_stack.getProperty("adhesion_type", "value") # Shell settings - print_settings["wall_line_count"] = global_container_stack.getProperty("wall_line_count", "value") - print_settings["retraction_enable"] = global_container_stack.getProperty("retraction_enable", "value") + print_settings["wall_line_count"] = global_stack.getProperty("wall_line_count", "value") + print_settings["retraction_enable"] = global_stack.getProperty("retraction_enable", "value") # Prime tower settings - print_settings["prime_tower_enable"] = global_container_stack.getProperty("prime_tower_enable", "value") + print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value") # Infill settings - print_settings["infill_sparse_density"] = global_container_stack.getProperty("infill_sparse_density", "value") - print_settings["infill_pattern"] = global_container_stack.getProperty("infill_pattern", "value") - print_settings["gradual_infill_steps"] = global_container_stack.getProperty("gradual_infill_steps", "value") + print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value") + print_settings["infill_pattern"] = global_stack.getProperty("infill_pattern", "value") + print_settings["gradual_infill_steps"] = global_stack.getProperty("gradual_infill_steps", "value") - print_settings["print_sequence"] = global_container_stack.getProperty("print_sequence", "value") + print_settings["print_sequence"] = global_stack.getProperty("print_sequence", "value") data["print_settings"] = print_settings diff --git a/plugins/SliceInfoPlugin/example_data.json b/plugins/SliceInfoPlugin/example_data.json new file mode 100644 index 0000000000..ec953e0842 --- /dev/null +++ b/plugins/SliceInfoPlugin/example_data.json @@ -0,0 +1,113 @@ +{ + "time_stamp": 1523973715.486928, + "schema_version": 0, + "cura_version": "3.3", + "active_mode": "custom", + "machine_settings_changed_by_user": true, + "language": "en_US", + "os": { + "type": "Linux", + "version": "#43~16.04.1-Ubuntu SMP Wed Mar 14 17:48:43 UTC 2018" + }, + "active_machine": { + "definition_id": "ultimaker3", + "manufacturer": "Ultimaker B.V." + }, + "extruders": [ + { + "active": true, + "material": { + "GUID": "506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9", + "type": "PLA", + "brand": "Generic" + }, + "material_used": 0.84, + "variant": "AA 0.4", + "nozzle_size": 0.4, + "extruder_settings": { + "wall_line_count": 3, + "retraction_enable": true, + "infill_sparse_density": 30, + "infill_pattern": "triangles", + "gradual_infill_steps": 0, + "default_material_print_temperature": 200, + "material_print_temperature": 200 + } + }, + { + "active": false, + "material": { + "GUID": "86a89ceb-4159-47f6-ab97-e9953803d70f", + "type": "PVA", + "brand": "Generic" + }, + "material_used": 0.5, + "variant": "BB 0.4", + "nozzle_size": 0.4, + "extruder_settings": { + "wall_line_count": 3, + "retraction_enable": true, + "infill_sparse_density": 20, + "infill_pattern": "triangles", + "gradual_infill_steps": 0, + "default_material_print_temperature": 215, + "material_print_temperature": 220 + } + } + ], + "quality_profile": "fast", + "models": [ + { + "hash": "b72789b9beb5366dff20b1cf501020c3d4d4df7dc2295ecd0fddd0a6436df070", + "bounding_box": { + "minimum": { + "x": -10.0, + "y": 0.0, + "z": -5.0 + }, + "maximum": { + "x": 9.999999046325684, + "y": 40.0, + "z": 5.0 + } + }, + "transformation": { + "data": "[[ 1. 0. 0. 0.] [ 0. 1. 0. 20.] [ 0. 0. 1. 0.] [ 0. 0. 0. 1.]]" + }, + "extruder": 0, + "model_settings": { + "support_enabled": true, + "support_extruder_nr": 1, + "infill_mesh": false, + "cutting_mesh": false, + "support_mesh": false, + "anti_overhang_mesh": false, + "wall_line_count": 3, + "retraction_enable": true, + "infill_sparse_density": 30, + "infill_pattern": "triangles", + "gradual_infill_steps": 0 + } + } + ], + "print_times": { + "travel": 187, + "support": 825, + "infill": 351, + "total": 7234 + }, + "print_settings": { + "layer_height": 0.15, + "support_enabled": true, + "support_extruder_nr": 1, + "adhesion_type": "brim", + "wall_line_count": 3, + "retraction_enable": true, + "prime_tower_enable": true, + "infill_sparse_density": 20, + "infill_pattern": "triangles", + "gradual_infill_steps": 0, + "print_sequence": "all_at_once" + }, + "output_to": "LocalFileOutputDevice" +} diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 1c7b2bcf7c..f4fa7c1557 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -7,6 +7,7 @@ import QtQuick.Layouts 1.1 import QtQuick.Controls.Styles 1.1 import UM 1.1 as UM +import Cura 1.0 as Cura UM.PreferencesPage { @@ -97,16 +98,26 @@ UM.PreferencesPage UM.Preferences.resetPreference("cura/choice_on_open_project") setDefaultOpenProjectOption(UM.Preferences.getValue("cura/choice_on_open_project")) - if (plugins.find("id", "SliceInfoPlugin") > -1) { + if (pluginExistsAndEnabled("SliceInfoPlugin")) { UM.Preferences.resetPreference("info/send_slice_info") sendDataCheckbox.checked = boolCheck(UM.Preferences.getValue("info/send_slice_info")) } - if (plugins.find("id", "UpdateChecker") > -1) { + if (pluginExistsAndEnabled("UpdateChecker")) { UM.Preferences.resetPreference("info/automatic_update_check") checkUpdatesCheckbox.checked = boolCheck(UM.Preferences.getValue("info/automatic_update_check")) } } + function pluginExistsAndEnabled(pluginName) + { + var pluginItem = plugins.find("id", pluginName) + if (pluginItem > -1) + { + return plugins.getItem(pluginItem).enabled + } + return false + } + ScrollView { width: parent.width @@ -366,7 +377,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width; height: childrenRect.height; text: catalog.i18nc("@info:tooltip", "Should zooming move in the direction of the mouse?") @@ -380,7 +392,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip", "Should models on the platform be moved so that they no longer intersect?") @@ -393,7 +406,8 @@ UM.PreferencesPage onCheckedChanged: UM.Preferences.setValue("physics/automatic_push_free", checked) } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip", "Should models on the platform be moved down to touch the build plate?") @@ -426,7 +440,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip", "Should layer be forced into compatibility mode?") @@ -453,7 +468,8 @@ UM.PreferencesPage text: catalog.i18nc("@label","Opening and saving files") } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip","Should models be scaled to the build volume if they are too large?") @@ -467,7 +483,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip","An model may appear extremely small if its unit is for example in meters rather than millimeters. Should these models be scaled up?") @@ -481,7 +498,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip", "Should a prefix based on the printer name be added to the print job name automatically?") @@ -495,7 +513,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip", "Should a summary be shown when saving a project file?") @@ -508,7 +527,8 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip", "Default behavior when opening a project file") @@ -531,7 +551,8 @@ UM.PreferencesPage { id: openProjectOptionModel - Component.onCompleted: { + Component.onCompleted: + { append({ text: catalog.i18nc("@option:openProject", "Always ask"), code: "always_ask" }) append({ text: catalog.i18nc("@option:openProject", "Always open as a project"), code: "open_as_project" }) append({ text: catalog.i18nc("@option:openProject", "Always import models"), code: "open_as_model" }) @@ -591,7 +612,8 @@ UM.PreferencesPage { id: discardOrKeepProfileListModel - Component.onCompleted: { + Component.onCompleted: + { append({ text: catalog.i18nc("@option:discardOrKeep", "Always ask me this"), code: "always_ask" }) append({ text: catalog.i18nc("@option:discardOrKeep", "Discard and never ask again"), code: "always_discard" }) append({ text: catalog.i18nc("@option:discardOrKeep", "Keep and never ask again"), code: "always_keep" }) @@ -631,8 +653,9 @@ UM.PreferencesPage text: catalog.i18nc("@label","Privacy") } - UM.TooltipArea { - visible: plugins.find("id", "UpdateChecker") > -1 + UM.TooltipArea + { + visible: pluginExistsAndEnabled("UpdateChecker") width: childrenRect.width height: visible ? childrenRect.height : 0 text: catalog.i18nc("@info:tooltip","Should Cura check for updates when the program is started?") @@ -646,8 +669,9 @@ UM.PreferencesPage } } - UM.TooltipArea { - visible: plugins.find("id", "SliceInfoPlugin") > -1 + UM.TooltipArea + { + visible: pluginExistsAndEnabled("SliceInfoPlugin") width: childrenRect.width height: visible ? childrenRect.height : 0 text: catalog.i18nc("@info:tooltip","Should anonymous data about your print be sent to Ultimaker? Note, no models, IP addresses or other personally identifiable information is sent or stored.") @@ -659,6 +683,17 @@ UM.PreferencesPage checked: boolCheck(UM.Preferences.getValue("info/send_slice_info")) onCheckedChanged: UM.Preferences.setValue("info/send_slice_info", checked) } + + Button + { + id: showMoreInfo + anchors.top: sendDataCheckbox.bottom + text: catalog.i18nc("@action:button", "More information") + onClicked: + { + CuraApplication.showMoreInformationDialogForAnonymousDataCollection(); + } + } } Item @@ -674,7 +709,8 @@ UM.PreferencesPage text: catalog.i18nc("@label","Experimental") } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height text: catalog.i18nc("@info:tooltip","Use multi build plate functionality") @@ -688,21 +724,29 @@ UM.PreferencesPage } } - UM.TooltipArea { + UM.TooltipArea + { width: childrenRect.width height: childrenRect.height - text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)") + text: catalog.i18nc("@info:tooltip", "Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)") CheckBox { id: arrangeOnLoadCheckbox - text: catalog.i18nc("@option:check","Do not arrange objects on load") + text: catalog.i18nc("@option:check", "Do not arrange objects on load") checked: boolCheck(UM.Preferences.getValue("cura/not_arrange_objects_on_load")) onCheckedChanged: UM.Preferences.setValue("cura/not_arrange_objects_on_load", checked) } } - + Connections + { + target: UM.Preferences + onPreferenceChanged: + { + sendDataCheckbox.checked = boolCheck(UM.Preferences.getValue("info/send_slice_info")) + } + } } } }