diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index f51174aec0..7700ee2e71 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -85,7 +85,7 @@ class CrashHandler: dialog = QDialog() dialog.setMinimumWidth(500) dialog.setMinimumHeight(170) - dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura can't startup")) + dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura can't start")) dialog.finished.connect(self._closeEarlyCrashDialog) layout = QVBoxLayout(dialog) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6d5bd34ee4..292d0bce94 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1288,7 +1288,7 @@ class CuraApplication(QtApplication): has_merged_nodes = False for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not isinstance(node, CuraSceneNode) or not node.getMeshData() : - if node.getName() == 'MergedMesh': + if node.getName() == "MergedMesh": has_merged_nodes = True continue @@ -1380,7 +1380,7 @@ class CuraApplication(QtApplication): # Use the previously found center of the group bounding box as the new location of the group group_node.setPosition(group_node.getBoundingBox().center) - group_node.setName("MergedMesh") # add a specific name to destinguis this node + group_node.setName("MergedMesh") # add a specific name to distinguish this node ## Updates origin position of all merged meshes @@ -1625,8 +1625,13 @@ class CuraApplication(QtApplication): node.setName(os.path.basename(filename)) self.getBuildVolume().checkBoundsAndUpdate(node) - extension = os.path.splitext(filename)[1] - if extension.lower() in self._non_sliceable_extensions: + is_non_sliceable = False + filename_lower = filename.lower() + for extension in self._non_sliceable_extensions: + if filename_lower.endswith(extension): + is_non_sliceable = True + break + if is_non_sliceable: self.callLater(lambda: self.getController().setActiveView("SimulationView")) block_slicing_decorator = BlockSlicingDecorator() diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 54abaca86e..c25b58fbcf 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -597,6 +597,18 @@ class MachineManager(QObject): if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved + ## Copy the value of all manually changed settings of the current extruder to all other extruders. + @pyqtSlot() + def copyAllValuesToExtruders(self): + extruder_stacks = list(self._global_container_stack.extruders.values()) + for extruder_stack in extruder_stacks: + if extruder_stack != self._active_container_stack: + for key in self._active_container_stack.userChanges.getAllKeys(): + new_value = self._active_container_stack.getProperty(key, "value") + + # check if the value has to be replaced + extruder_stack.userChanges.setProperty(key, "value", new_value) + @pyqtProperty(str, notify = activeVariantChanged) def activeVariantName(self) -> str: if self._active_container_stack: diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e55abe59a2..9873d91c05 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -119,6 +119,7 @@ class CuraEngineBackend(QObject, Backend): self._postponed_scene_change_sources = [] # scene change is postponed (by a tool) self._slice_start_time = None + self._is_disabled = False Preferences.getInstance().addPreference("general/auto_slice", True) @@ -405,6 +406,7 @@ class CuraEngineBackend(QObject, Backend): # - decorator isBlockSlicing is found (used in g-code reader) def determineAutoSlicing(self): enable_timer = True + self._is_disabled = False if not Preferences.getInstance().getValue("general/auto_slice"): enable_timer = False @@ -412,6 +414,7 @@ class CuraEngineBackend(QObject, Backend): if node.callDecoration("isBlockSlicing"): enable_timer = False self.backendStateChange.emit(BackendState.Disabled) + self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list @@ -545,6 +548,10 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.stop() def _onStackErrorCheckFinished(self): + self.determineAutoSlicing() + if self._is_disabled: + return + if not self._slicing and self._build_plates_to_be_sliced: self.needsSlicing() self._onChanged() diff --git a/plugins/GCodeGzReader/GCodeGzReader.py b/plugins/GCodeGzReader/GCodeGzReader.py new file mode 100644 index 0000000000..9d671a70bf --- /dev/null +++ b/plugins/GCodeGzReader/GCodeGzReader.py @@ -0,0 +1,33 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import gzip +import tempfile + +from io import StringIO, BufferedIOBase #To write the g-code to a temporary buffer, and for typing. +from typing import List + +from UM.Logger import Logger +from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementing. +from UM.PluginRegistry import PluginRegistry +from UM.Scene.SceneNode import SceneNode #For typing. + +## A file reader that reads gzipped g-code. +# +# If you're zipping g-code, you might as well use gzip! +class GCodeGzReader(MeshReader): + + def __init__(self): + super().__init__() + self._supported_extensions = [".gcode.gz"] + + def read(self, file_name): + with open(file_name, "rb") as file: + file_data = file.read() + uncompressed_gcode = gzip.decompress(file_data) + with tempfile.NamedTemporaryFile() as temp_file: + temp_file.write(uncompressed_gcode) + PluginRegistry.getInstance().getPluginObject("GCodeReader").preRead(temp_file.name) + result = PluginRegistry.getInstance().getPluginObject("GCodeReader").read(temp_file.name) + + return result diff --git a/plugins/GCodeGzReader/__init__.py b/plugins/GCodeGzReader/__init__.py new file mode 100644 index 0000000000..98965c00aa --- /dev/null +++ b/plugins/GCodeGzReader/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from . import GCodeGzReader + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + +def getMetaData(): + return { + "mesh_reader": [ + { + "extension": "gcode.gz", + "description": i18n_catalog.i18nc("@item:inlistbox", "Compressed G-code File") + } + ] + } + +def register(app): + app.addNonSliceableExtension(".gcode.gz") + return { "mesh_reader": GCodeGzReader.GCodeGzReader() } diff --git a/plugins/GCodeGzReader/plugin.json b/plugins/GCodeGzReader/plugin.json new file mode 100644 index 0000000000..e9f14724e0 --- /dev/null +++ b/plugins/GCodeGzReader/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Compressed G-code Reader", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "description": "Reads g-code from a compressed archive.", + "api": 4, + "i18n-catalog": "cura" +} diff --git a/plugins/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py new file mode 100644 index 0000000000..8a501ceb27 --- /dev/null +++ b/plugins/ModelChecker/ModelChecker.py @@ -0,0 +1,116 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os + +from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty + +from UM.Application import Application +from UM.Extension import Extension +from UM.Logger import Logger +from UM.Message import Message +from UM.i18n import i18nCatalog +from UM.PluginRegistry import PluginRegistry +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator + +catalog = i18nCatalog("cura") + + +class ModelChecker(QObject, Extension): + ## Signal that gets emitted when anything changed that we need to check. + onChanged = pyqtSignal() + + def __init__(self): + super().__init__() + + self._button_view = None + + self._caution_message = Message("", #Message text gets set when the message gets shown, to display the models in question. + lifetime = 0, + title = catalog.i18nc("@info:title", "Model Checker Warning")) + + Application.getInstance().initializationFinished.connect(self._pluginsInitialized) + Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged) + + ## Pass-through to allow UM.Signal to connect with a pyqtSignal. + def _onChanged(self, _): + self.onChanged.emit() + + ## Called when plug-ins are initialized. + # + # This makes sure that we listen to changes of the material and that the + # button is created that indicates warnings with the current set-up. + def _pluginsInitialized(self): + Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged) + self._createView() + + def checkObjectsForShrinkage(self): + shrinkage_threshold = 0.5 #From what shrinkage percentage a warning will be issued about the model size. + warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials. + warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials. + + material_shrinkage = self._getMaterialShrinkage() + + warning_nodes = [] + + # Check node material shrinkage and bounding box size + for node in self.sliceableNodes(): + node_extruder_position = node.callDecoration("getActiveExtruderPosition") + if material_shrinkage[node_extruder_position] > shrinkage_threshold: + bbox = node.getBoundingBox() + if bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z: + warning_nodes.append(node) + + self._caution_message.setText(catalog.i18nc( + "@info:status", + "Some models may not be printed optimal due to object size and chosen material for models: {model_names}.\n" + "Tips that may be useful to improve the print quality:\n" + "1) Use rounded corners\n" + "2) Turn the fan off (only if the are no tiny details on the model)\n" + "3) Use a different material").format(model_names = ", ".join([n.getName() for n in warning_nodes]))) + + return len(warning_nodes) > 0 + + def sliceableNodes(self): + # Add all sliceable scene nodes to check + scene = Application.getInstance().getController().getScene() + for node in DepthFirstIterator(scene.getRoot()): + if node.callDecoration("isSliceable"): + yield node + + ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. + def _createView(self): + Logger.log("d", "Creating model checker view.") + + # Create the plugin dialog component + path = os.path.join(PluginRegistry.getInstance().getPluginPath("ModelChecker"), "ModelChecker.qml") + self._button_view = Application.getInstance().createQmlComponent(path, {"manager": self}) + + # The qml is only the button + Application.getInstance().addAdditionalComponent("jobSpecsButton", self._button_view) + + Logger.log("d", "Model checker view created.") + + @pyqtProperty(bool, notify = onChanged) + def runChecks(self): + danger_shrinkage = self.checkObjectsForShrinkage() + + return any((danger_shrinkage, )) #If any of the checks fail, show the warning button. + + @pyqtSlot() + def showWarnings(self): + self._caution_message.show() + + def _getMaterialShrinkage(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack is None: + return {} + + material_shrinkage = {} + # Get all shrinkage values of materials used + for extruder_position, extruder in global_container_stack.extruders.items(): + shrinkage = extruder.material.getProperty("material_shrinkage_percentage", "value") + if shrinkage is None: + shrinkage = 0 + material_shrinkage[extruder_position] = shrinkage + return material_shrinkage diff --git a/plugins/ModelChecker/ModelChecker.qml b/plugins/ModelChecker/ModelChecker.qml new file mode 100644 index 0000000000..3db54d4387 --- /dev/null +++ b/plugins/ModelChecker/ModelChecker.qml @@ -0,0 +1,43 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 + +import UM 1.2 as UM +import Cura 1.0 as Cura + + +Button +{ + id: modelCheckerButton + + UM.I18nCatalog{id: catalog; name:"cura"} + + visible: manager.runChecks + tooltip: catalog.i18nc("@info:tooltip", "Some things could be problematic in this print. Click to see tips for adjustment.") + onClicked: manager.showWarnings() + + width: UM.Theme.getSize("save_button_specs_icons").width + height: UM.Theme.getSize("save_button_specs_icons").height + + style: ButtonStyle + { + background: Item + { + UM.RecolorImage + { + width: UM.Theme.getSize("save_button_specs_icons").width; + height: UM.Theme.getSize("save_button_specs_icons").height; + sourceSize.width: width; + sourceSize.height: width; + color: control.hovered ? UM.Theme.getColor("text_scene_hover") : UM.Theme.getColor("text_scene"); + source: "model_checker.svg" + } + } + } +} diff --git a/plugins/ModelChecker/__init__.py b/plugins/ModelChecker/__init__.py new file mode 100644 index 0000000000..5f4d443729 --- /dev/null +++ b/plugins/ModelChecker/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2018 Ultimaker B.V. +# This example is released under the terms of the AGPLv3 or higher. + +from . import ModelChecker + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + + +def getMetaData(): + return {} + +def register(app): + return { "extension": ModelChecker.ModelChecker() } diff --git a/plugins/ModelChecker/model_checker.svg b/plugins/ModelChecker/model_checker.svg new file mode 100644 index 0000000000..ce9594302e --- /dev/null +++ b/plugins/ModelChecker/model_checker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/ModelChecker/plugin.json b/plugins/ModelChecker/plugin.json new file mode 100644 index 0000000000..a9190adcaa --- /dev/null +++ b/plugins/ModelChecker/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Model Checker", + "author": "Ultimaker B.V.", + "version": "0.1", + "api": 4, + "description": "Checks models and print configuration for possible printing issues and give suggestions.", + "i18n-catalog": "cura" +} diff --git a/plugins/PluginBrowser/PluginBrowser.py b/plugins/PluginBrowser/PluginBrowser.py index c8a5e1e545..bb4d5fb395 100644 --- a/plugins/PluginBrowser/PluginBrowser.py +++ b/plugins/PluginBrowser/PluginBrowser.py @@ -1,11 +1,10 @@ # Copyright (c) 2017 Ultimaker B.V. # PluginBrowser is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from UM.Application import Application -from UM.Qt.ListModel import ListModel from UM.Logger import Logger from UM.PluginRegistry import PluginRegistry from UM.Qt.Bindings.PluginsModel import PluginsModel @@ -20,7 +19,6 @@ import os import tempfile import platform import zipfile -import shutil from cura.CuraApplication import CuraApplication @@ -44,7 +42,7 @@ class PluginBrowser(QObject, Extension): self._plugins_metadata = [] self._plugins_model = None - # Can be 'installed' or 'availble' + # Can be 'installed' or 'available' self._view = "available" self._restart_required = False diff --git a/plugins/PluginBrowser/PluginEntry.qml b/plugins/PluginBrowser/PluginEntry.qml index eff9eb8943..9dbcb96e79 100644 --- a/plugins/PluginBrowser/PluginEntry.qml +++ b/plugins/PluginBrowser/PluginEntry.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Ultimaker B.V. +// Copyright (c) 2018 Ultimaker B.V. // PluginBrowser is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -129,6 +129,18 @@ Component { return catalog.i18nc("@action:button", "Install"); } } + enabled: + { + if ( manager.isDownloading ) + { + return pluginList.activePlugin == model ? true : false + } + else + { + return true + } + } + opacity: enabled ? 1.0 : 0.5 visible: model.external && ((model.status !== "installed") || model.can_upgrade) style: ButtonStyle { background: Rectangle { diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 3697e38661..85849efb2f 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -74,7 +74,7 @@ class SimulationView(View): self._global_container_stack = None self._proxy = SimulationViewProxy() - self._controller.getScene().sceneChanged.connect(self._onSceneChanged) + self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self._resetSettings() self._legend_items = None @@ -160,10 +160,10 @@ class SimulationView(View): def _onSceneChanged(self, node): if node.getMeshData() is None: self.resetLayerData() - else: - self.setActivity(False) - self.calculateMaxLayers() - self.calculateMaxPathsOnLayer(self._current_layer_num) + + self.setActivity(False) + self.calculateMaxLayers() + self.calculateMaxPathsOnLayer(self._current_layer_num) def isBusy(self): return self._busy diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 72f5260249..f3667fc2f3 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -127,10 +127,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._sending_job.send("") #No specifically selected printer. is_job_sent = self._sending_job.send(None) - # Notify the UI that a switch to the print monitor should happen - if is_job_sent: - Application.getInstance().getController().setActiveStage("MonitorStage") - def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml") @@ -242,6 +238,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if new_progress > self._progress_message.getProgress(): self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) + + # If successfully sent: + if bytes_sent == bytes_total: + # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the + # monitor tab. + self._success_message = Message( + i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), + lifetime=5, dismissable=True, + title=i18n_catalog.i18nc("@info:title", "Data Sent")) + self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Montior"), icon=None, + description="") + self._success_message.actionTriggered.connect(self._successMessageActionTriggered) + self._success_message.show() else: self._progress_message.setProgress(0) self._progress_message.hide() @@ -260,6 +269,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._latest_reply_handler.disconnect() self._latest_reply_handler = None + def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None: + if action_id == "View": + Application.getInstance().getController().setActiveStage("MonitorStage") @pyqtSlot() def openPrintJobControlPanel(self) -> None: diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index c51ef97bf0..54caca855e 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -1032,7 +1032,8 @@ class XmlMaterialProfile(InstanceContainer): "retraction amount": "retraction_amount", "retraction speed": "retraction_speed", "adhesion tendency": "material_adhesion_tendency", - "surface energy": "material_surface_energy" + "surface energy": "material_surface_energy", + "shrinkage percentage": "material_shrinkage_percentage", } __unmapped_settings = [ "hardware compatible", diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 21ee543333..d7d9698439 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2097,6 +2097,19 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "material_shrinkage_percentage": + { + "label": "Shrinkage Ratio", + "description": "Shrinkage ratio in percentage.", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": "0", + "maximum_value": "100", + "enabled": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, "material_flow": { "label": "Flow", diff --git a/resources/i18n/de_DE/cura.po b/resources/i18n/de_DE/cura.po index 250e3e5e1b..f398d1ec4e 100644 --- a/resources/i18n/de_DE/cura.po +++ b/resources/i18n/de_DE/cura.po @@ -194,7 +194,7 @@ msgstr "Vorbereiten" #: /home/ruben/Projects/Cura/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py:23 msgctxt "@action:button Preceded by 'Ready to'." msgid "Save to Removable Drive" -msgstr "Speichern auf Wechseldatenträger" +msgstr "Speichern auf Datenträger" #: /home/ruben/Projects/Cura/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py:24 #, python-brace-format diff --git a/resources/qml/JobSpecs.qml b/resources/qml/JobSpecs.qml index 742e8d6765..3238c66a1e 100644 --- a/resources/qml/JobSpecs.qml +++ b/resources/qml/JobSpecs.qml @@ -115,15 +115,50 @@ Item { } } + Row { + id: additionalComponentsRow + anchors.top: jobNameRow.bottom + anchors.right: parent.right + } + Label { id: boundingSpec anchors.top: jobNameRow.bottom - anchors.right: parent.right + anchors.right: additionalComponentsRow.left + anchors.rightMargin: + { + if (additionalComponentsRow.width > 0) + { + return UM.Theme.getSize("default_margin").width + } + else + { + return 0; + } + } height: UM.Theme.getSize("jobspecs_line").height verticalAlignment: Text.AlignVCenter font: UM.Theme.getFont("small") color: UM.Theme.getColor("text_scene") text: CuraApplication.getSceneBoundingBoxString } + + Component.onCompleted: { + base.addAdditionalComponents("jobSpecsButton") + } + + Connections { + target: CuraApplication + onAdditionalComponentsChanged: base.addAdditionalComponents("jobSpecsButton") + } + + function addAdditionalComponents (areaId) { + if(areaId == "jobSpecsButton") { + for (var component in CuraApplication.additionalComponents["jobSpecsButton"]) { + CuraApplication.additionalComponents["jobSpecsButton"][component].parent = additionalComponentsRow + } + } + } + } diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml index 331d78ead9..7aaf87b4df 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml @@ -21,7 +21,10 @@ Column { // FIXME For now the model should be removed and then created again, otherwise changes in the printer don't automatically update the UI configurationList.model = [] - configurationList.model = outputDevice.uniqueConfigurations + if(outputDevice) + { + configurationList.model = outputDevice.uniqueConfigurations + } } Label diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 199db1bbaa..4a919ab9bd 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -533,6 +533,15 @@ Item onTriggered: Cura.MachineManager.copyValueToExtruders(contextMenu.key) } + MenuItem + { + //: Settings context menu action + text: catalog.i18nc("@action:menu", "Copy all changed values to all extruders") + visible: machineExtruderCount.properties.value > 1 + enabled: contextMenu.provider != undefined + onTriggered: Cura.MachineManager.copyAllValuesToExtruders() + } + MenuSeparator { visible: machineExtruderCount.properties.value > 1 diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 4f4b2306a8..0fde7f3bc9 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -411,6 +411,8 @@ "save_button_save_to_button": [0.3, 2.7], "save_button_specs_icons": [1.4, 1.4], + "job_specs_button": [2.7, 2.7], + "monitor_preheat_temperature_control": [4.5, 2.0], "modal_window_minimum": [60.0, 45],