diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 0b81a5183f..d93ce1107d 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -136,6 +136,7 @@ class BuildVolume(SceneNode): if active_extruder_changed is not None: node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild) node.decoratorsChanged.disconnect(self._updateNodeListeners) + self._updateDisallowedAreasAndRebuild() # make sure we didn't miss anything before we updated the node listeners self._scene_objects = new_scene_objects self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. @@ -150,7 +151,6 @@ class BuildVolume(SceneNode): active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal") if active_extruder_changed is not None: active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild) - self._updateDisallowedAreasAndRebuild() def setWidth(self, width): if width is not None: diff --git a/cura/CuraActions.py b/cura/CuraActions.py index f517ec4217..75338f17b6 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -109,10 +109,6 @@ class CuraActions(QObject): nodes_to_change = [] for node in Selection.getAllSelectedObjects(): - # Do not change any nodes that already have the right extruder set. - if node.callDecoration("getActiveExtruder") == extruder_id: - continue - # If the node is a group, apply the active extruder to all children of the group. if node.callDecoration("isGroup"): for grouped_node in BreadthFirstIterator(node): @@ -125,6 +121,10 @@ class CuraActions(QObject): nodes_to_change.append(grouped_node) continue + # Do not change any nodes that already have the right extruder set. + if node.callDecoration("getActiveExtruder") == extruder_id: + continue + nodes_to_change.append(node) if not nodes_to_change: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5d5d04b716..6056745c75 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,10 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -#Type hinting. -from typing import Dict - -from PyQt5.QtCore import QObject +from PyQt5.QtCore import QObject, QTimer from PyQt5.QtNetwork import QLocalServer from PyQt5.QtNetwork import QLocalSocket @@ -68,6 +65,8 @@ from cura.Machines.Models.QualityManagementModel import QualityManagementModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.MachineManagementModel import MachineManagementModel +from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel + from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Settings.SettingInheritanceManager import SettingInheritanceManager @@ -91,7 +90,6 @@ from cura.Settings.UserChangesModel import UserChangesModel from cura.Settings.ExtrudersModel import ExtrudersModel from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.ContainerManager import ContainerManager -from cura.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.ObjectsModel import ObjectsModel @@ -101,7 +99,6 @@ from PyQt5.QtGui import QColor, QIcon from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType -from configparser import ConfigParser import sys import os.path import numpy @@ -226,6 +223,7 @@ class CuraApplication(QtApplication): self._object_manager = None self._build_plate_model = None self._multi_build_plate_model = None + self._setting_visibility_presets_model = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None self._cura_scene_controller = None @@ -286,10 +284,15 @@ class CuraApplication(QtApplication): self._preferred_mimetype = "" self._i18n_catalog = i18nCatalog("cura") - self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity) + self._update_platform_activity_timer = QTimer() + self._update_platform_activity_timer.setInterval(500) + self._update_platform_activity_timer.setSingleShot(True) + self._update_platform_activity_timer.timeout.connect(self.updatePlatformActivity) + + self.getController().getScene().sceneChanged.connect(self.updatePlatformActivityDelayed) self.getController().toolOperationStopped.connect(self._onToolOperationStopped) self.getController().contextMenuRequested.connect(self._onContextMenuRequested) - self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity) + self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed) Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.Firmware, "firmware") @@ -376,10 +379,6 @@ class CuraApplication(QtApplication): preferences.setDefault("local_file/last_used_type", "text/x-gcode") - default_visibility_profile = SettingVisibilityPresetsModel.getInstance().getItem(0) - - preferences.setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) - self.applicationShuttingDown.connect(self.saveSettings) self.engineCreatedSignal.connect(self._onEngineCreated) @@ -452,27 +451,18 @@ class CuraApplication(QtApplication): @pyqtSlot(str) def discardOrKeepProfileChangesClosed(self, option): + global_stack = self.getGlobalContainerStack() if option == "discard": - global_stack = self.getGlobalContainerStack() - for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()): - extruder.getTop().clear() - global_stack.getTop().clear() + for extruder in global_stack.extruders.values(): + extruder.userChanges.clear() + global_stack.userChanges.clear() # if the user decided to keep settings then the user settings should be re-calculated and validated for errors # before slicing. To ensure that slicer uses right settings values elif option == "keep": - global_stack = self.getGlobalContainerStack() - for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()): - user_extruder_container = extruder.getTop() - if user_extruder_container: - user_extruder_container.update() - - user_global_container = global_stack.getTop() - if user_global_container: - user_global_container.update() - - # notify listeners that quality has changed (after user selected discard or keep) - self.getMachineManager().activeQualityChanged.emit() + for extruder in global_stack.extruders.values(): + extruder.userChanges.update() + global_stack.userChanges.update() @pyqtSlot(int) def messageBoxClosed(self, button): @@ -682,6 +672,11 @@ class CuraApplication(QtApplication): self._print_information = PrintInformation.PrintInformation() self._cura_actions = CuraActions.CuraActions(self) + # Initialize setting visibility presets model + self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self) + default_visibility_profile = self._setting_visibility_presets_model.getItem(0) + Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) + # Detect in which mode to run and execute that mode if self.getCommandLineOption("headless", False): self.runWithoutGUI() @@ -764,6 +759,10 @@ class CuraApplication(QtApplication): def hasGui(self): return self._use_gui + @pyqtSlot(result = QObject) + def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel: + return self._setting_visibility_presets_model + def getMachineErrorChecker(self, *args) -> MachineErrorChecker: return self._machine_error_checker @@ -890,11 +889,11 @@ class CuraApplication(QtApplication): qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel") qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler") + qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel") qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel") qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager) - qmlRegisterSingletonType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel", SettingVisibilityPresetsModel.createSettingVisibilityPresetsModel) # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work. actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) @@ -970,6 +969,10 @@ class CuraApplication(QtApplication): def getSceneBoundingBoxString(self): return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()} + def updatePlatformActivityDelayed(self, node = None): + if node is not None and node.getMeshData() is not None: + self._update_platform_activity_timer.start() + ## Update scene bounding box for current build plate def updatePlatformActivity(self, node = None): count = 0 diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index 0a82fcc764..b854dbf29e 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -107,6 +107,7 @@ class MaterialManager(QObject): # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() + material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): if not self._container_registry.isReadOnly(root_material_id): continue @@ -114,6 +115,7 @@ class MaterialManager(QObject): if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} + material_types_without_fallback.add(material_type) brand = material_node.root_material_node.metadata["brand"] if brand.lower() == "generic": to_add = True @@ -123,6 +125,10 @@ class MaterialManager(QObject): to_add = False # don't add if it's not the default diameter if to_add: grouped_by_type_dict[material_type] = material_node.root_material_node.metadata + material_types_without_fallback.remove(material_type) + # Remove the materials that have no fallback materials + for material_type in material_types_without_fallback: + del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 diff --git a/cura/Machines/Models/MachineManagementModel.py b/cura/Machines/Models/MachineManagementModel.py index 481a692675..7dc51f07f7 100644 --- a/cura/Machines/Models/MachineManagementModel.py +++ b/cura/Machines/Models/MachineManagementModel.py @@ -3,7 +3,7 @@ from UM.Qt.ListModel import ListModel -from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, pyqtSignal +from PyQt5.QtCore import Qt from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerStack import ContainerStack @@ -11,6 +11,7 @@ from UM.Settings.ContainerStack import ContainerStack from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") + # # This the QML model for the quality management page. # @@ -39,7 +40,7 @@ class MachineManagementModel(ListModel): ## Handler for container added/removed events from registry def _onContainerChanged(self, container): # We only need to update when the added / removed container is a stack. - if isinstance(container, ContainerStack): + if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine": self._update() ## Private convenience function to reset & repopulate the model. @@ -47,7 +48,9 @@ class MachineManagementModel(ListModel): items = [] # Get first the network enabled printers - network_filter_printers = {"type": "machine", "um_network_key": "*", "hidden": "False"} + network_filter_printers = {"type": "machine", + "um_network_key": "*", + "hidden": "False"} self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers) self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("connect_group_name")) @@ -57,11 +60,11 @@ class MachineManagementModel(ListModel): metadata["definition_name"] = container.getBottom().getName() items.append({"name": metadata["connect_group_name"], - "id": container.getId(), - "metadata": metadata, - "group": catalog.i18nc("@info:title", "Network enabled printers")}) + "id": container.getId(), + "metadata": metadata, + "group": catalog.i18nc("@info:title", "Network enabled printers")}) - # Get now the local printes + # Get now the local printers local_filter_printers = {"type": "machine", "um_network_key": None} self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers) self._local_container_stacks.sort(key = lambda i: i.getName()) @@ -72,8 +75,8 @@ class MachineManagementModel(ListModel): metadata["definition_name"] = container.getBottom().getName() items.append({"name": container.getName(), - "id": container.getId(), - "metadata": metadata, - "group": catalog.i18nc("@info:title", "Local printers")}) + "id": container.getId(), + "metadata": metadata, + "group": catalog.i18nc("@info:title", "Local printers")}) self.setItems(items) diff --git a/cura/Machines/Models/MultiBuildPlateModel.py b/cura/Machines/Models/MultiBuildPlateModel.py index f0f4997014..958e93837a 100644 --- a/cura/Machines/Models/MultiBuildPlateModel.py +++ b/cura/Machines/Models/MultiBuildPlateModel.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtSignal, pyqtProperty +from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty from UM.Application import Application from UM.Scene.Selection import Selection @@ -21,8 +21,13 @@ class MultiBuildPlateModel(ListModel): def __init__(self, parent = None): super().__init__(parent) + self._update_timer = QTimer() + self._update_timer.setInterval(100) + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._updateSelectedObjectBuildPlateNumbers) + self._application = Application.getInstance() - self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers) + self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbersDelayed) Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers) self._max_build_plate = 1 # default @@ -45,6 +50,9 @@ class MultiBuildPlateModel(ListModel): def activeBuildPlate(self): return self._active_build_plate + def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args): + self._update_timer.start() + def _updateSelectedObjectBuildPlateNumbers(self, *args): result = set() for node in Selection.getAllSelectedObjects(): diff --git a/cura/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py new file mode 100644 index 0000000000..599dd982a8 --- /dev/null +++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py @@ -0,0 +1,178 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional +import os +import urllib.parse +from configparser import ConfigParser + +from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot + +from UM.Logger import Logger +from UM.Qt.ListModel import ListModel +from UM.Preferences import Preferences +from UM.Resources import Resources +from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + + +class SettingVisibilityPresetsModel(ListModel): + IdRole = Qt.UserRole + 1 + NameRole = Qt.UserRole + 2 + SettingsRole = Qt.UserRole + 3 + + def __init__(self, parent = None): + super().__init__(parent) + self.addRoleName(self.IdRole, "id") + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.SettingsRole, "settings") + + self._populate() + basic_item = self.items[1] + basic_visibile_settings = ";".join(basic_item["settings"]) + + self._preferences = Preferences.getInstance() + # Preference to store which preset is currently selected + self._preferences.addPreference("cura/active_setting_visibility_preset", "basic") + # Preference that stores the "custom" set so it can always be restored (even after a restart) + self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings) + self._preferences.preferenceChanged.connect(self._onPreferencesChanged) + + self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset")) + # Initialize visible settings if it is not done yet + visible_settings = self._preferences.getValue("general/visible_settings") + if not visible_settings: + self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"])) + else: + self._onPreferencesChanged("general/visible_settings") + + self.activePresetChanged.emit() + + def _getItem(self, item_id: str) -> Optional[dict]: + result = None + for item in self.items: + if item["id"] == item_id: + result = item + break + return result + + def _populate(self): + from cura.CuraApplication import CuraApplication + items = [] + for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset): + try: + mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) + except MimeTypeNotFoundError: + Logger.log("e", "Could not determine mime type of file %s", file_path) + continue + + item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path))) + if not os.path.isfile(file_path): + Logger.log("e", "[%s] is not a file", file_path) + continue + + parser = ConfigParser(allow_no_value = True) # accept options without any value, + try: + parser.read([file_path]) + if not parser.has_option("general", "name") or not parser.has_option("general", "weight"): + continue + + settings = [] + for section in parser.sections(): + if section == 'general': + continue + + settings.append(section) + for option in parser[section].keys(): + settings.append(option) + + items.append({ + "id": item_id, + "name": catalog.i18nc("@action:inmenu", parser["general"]["name"]), + "weight": parser["general"]["weight"], + "settings": settings, + }) + + except Exception: + Logger.logException("e", "Failed to load setting preset %s", file_path) + + items.sort(key = lambda k: (int(k["weight"]), k["id"])) + # Put "custom" at the top + items.insert(0, {"id": "custom", + "name": "Custom selection", + "weight": -100, + "settings": []}) + + self.setItems(items) + + @pyqtSlot(str) + def setActivePreset(self, preset_id: str): + if preset_id == self._active_preset_item["id"]: + Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id) + return + + preset_item = None + for item in self.items: + if item["id"] == preset_id: + preset_item = item + break + if preset_item is None: + Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id) + return + + need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom" + if need_to_save_to_custom: + # Save the current visibility settings to custom + current_visibility_string = self._preferences.getValue("general/visible_settings") + if current_visibility_string: + self._preferences.setValue("cura/custom_visible_settings", current_visibility_string) + + new_visibility_string = ";".join(preset_item["settings"]) + if preset_id == "custom": + # Get settings from the stored custom data + new_visibility_string = self._preferences.getValue("cura/custom_visible_settings") + if new_visibility_string is None: + new_visibility_string = self._preferences.getValue("general/visible_settings") + self._preferences.setValue("general/visible_settings", new_visibility_string) + + self._preferences.setValue("cura/active_setting_visibility_preset", preset_id) + self._active_preset_item = preset_item + self.activePresetChanged.emit() + + activePresetChanged = pyqtSignal() + + @pyqtProperty(str, notify = activePresetChanged) + def activePreset(self) -> str: + return self._active_preset_item["id"] + + def _onPreferencesChanged(self, name: str): + if name != "general/visible_settings": + return + + # Find the preset that matches with the current visible settings setup + visibility_string = self._preferences.getValue("general/visible_settings") + if not visibility_string: + return + + visibility_set = set(visibility_string.split(";")) + matching_preset_item = None + for item in self.items: + if item["id"] == "custom": + continue + if set(item["settings"]) == visibility_set: + matching_preset_item = item + break + + if matching_preset_item is None: + # The new visibility setup is "custom" should be custom + if self._active_preset_item["id"] == "custom": + # We are already in custom, just save the settings + self._preferences.setValue("cura/custom_visible_settings", visibility_string) + else: + self._active_preset_item = self.items[0] # 0 is custom + self.activePresetChanged.emit() + else: + self._active_preset_item = matching_preset_item + self.activePresetChanged.emit() diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index f02e8b4db5..cfe4320e28 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -1,3 +1,8 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import QTimer + from UM.Application import Application from UM.Qt.ListModel import ListModel from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -14,8 +19,13 @@ class ObjectsModel(ListModel): def __init__(self): super().__init__() - Application.getInstance().getController().getScene().sceneChanged.connect(self._update) - Preferences.getInstance().preferenceChanged.connect(self._update) + Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed) + Preferences.getInstance().preferenceChanged.connect(self._updateDelayed) + + self._update_timer = QTimer() + self._update_timer.setInterval(100) + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._update) self._build_plate_number = -1 @@ -23,6 +33,9 @@ class ObjectsModel(ListModel): self._build_plate_number = nr self._update() + def _updateDelayed(self, *args): + self._update_timer.start() + def _update(self, *args): nodes = [] filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") diff --git a/cura/PickingPass.py b/cura/PickingPass.py new file mode 100644 index 0000000000..2a1abe8f63 --- /dev/null +++ b/cura/PickingPass.py @@ -0,0 +1,69 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from UM.Application import Application +from UM.Math.Vector import Vector +from UM.Resources import Resources + +from UM.View.RenderPass import RenderPass +from UM.View.GL.OpenGL import OpenGL +from UM.View.RenderBatch import RenderBatch + +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator + + +## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture. +# The texture is used to map a 2d location (eg the mouse location) to a world space position +# +# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels +class PickingPass(RenderPass): + def __init__(self, width: int, height: int): + super().__init__("picking", width, height) + + self._renderer = Application.getInstance().getRenderer() + + self._shader = None + self._scene = Application.getInstance().getController().getScene() + + def render(self) -> None: + if not self._shader: + self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader")) + + width, height = self.getSize() + self._gl.glViewport(0, 0, width, height) + self._gl.glClearColor(1.0, 1.0, 1.0, 0.0) + self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT) + + # Create a new batch to be rendered + batch = RenderBatch(self._shader) + + # Fill up the batch with objects that can be sliced. ` + for node in DepthFirstIterator(self._scene.getRoot()): + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): + batch.addItem(node.getWorldTransformation(), node.getMeshData()) + + self.bind() + batch.render(self._scene.getActiveCamera()) + self.release() + + ## Get the distance in mm from the camera to at a certain pixel coordinate. + def getPickedDepth(self, x: int, y: int) -> float: + output = self.getOutput() + + window_size = self._renderer.getWindowSize() + + px = (0.5 + x / 2.0) * window_size[0] + py = (0.5 + y / 2.0) * window_size[1] + + if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1): + return -1 + + distance = output.pixel(px, py) # distance in micron, from in r, g & b channels + distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm + return distance + + ## Get the world coordinates of a picked point + def getPickedPosition(self, x: int, y: int) -> Vector: + distance = self.getPickedDepth(x, y) + ray = self._scene.getActiveCamera().getRay(x, y) + + return ray.getPointAlongRay(distance) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 3d9d5d5027..1a5d6ef837 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -71,7 +71,7 @@ class PlatformPhysics: # Move it downwards if bottom is above platform move_vector = Vector() - if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down + if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 move_vector = move_vector.set(y = -bbox.bottom + z_offset) diff --git a/cura/PrinterOutput/ExtruderOutputModel.py b/cura/PrinterOutput/ExtruderOutputModel.py index e4c7f1608e..75b9cc98ac 100644 --- a/cura/PrinterOutput/ExtruderOutputModel.py +++ b/cura/PrinterOutput/ExtruderOutputModel.py @@ -18,6 +18,7 @@ class ExtruderOutputModel(QObject): hotendTemperatureChanged = pyqtSignal() activeMaterialChanged = pyqtSignal() extruderConfigurationChanged = pyqtSignal() + isPreheatingChanged = pyqtSignal() def __init__(self, printer: "PrinterOutputModel", position, parent=None): super().__init__(parent) @@ -30,6 +31,21 @@ class ExtruderOutputModel(QObject): self._extruder_configuration = ExtruderConfigurationModel() self._extruder_configuration.position = self._position + self._is_preheating = False + + def getPrinter(self): + return self._printer + + def getPosition(self): + return self._position + + # Does the printer support pre-heating the bed at all + @pyqtProperty(bool, constant=True) + def canPreHeatHotends(self): + if self._printer: + return self._printer.canPreHeatHotends + return False + @pyqtProperty(QObject, notify = activeMaterialChanged) def activeMaterial(self) -> "MaterialOutputModel": return self._active_material @@ -82,3 +98,25 @@ class ExtruderOutputModel(QObject): if self._extruder_configuration.isValid(): return self._extruder_configuration return None + + def updateIsPreheating(self, pre_heating): + if self._is_preheating != pre_heating: + self._is_preheating = pre_heating + self.isPreheatingChanged.emit() + + @pyqtProperty(bool, notify=isPreheatingChanged) + def isPreheating(self): + return self._is_preheating + + ## Pre-heats the extruder before printer. + # + # \param temperature The temperature to heat the extruder to, in degrees + # Celsius. + # \param duration How long the bed should stay warm, in seconds. + @pyqtSlot(float, float) + def preheatHotend(self, temperature, duration): + self._printer._controller.preheatHotend(self, temperature, duration) + + @pyqtSlot() + def cancelPreheatHotend(self): + self._printer._controller.cancelPreheatHotend(self) \ No newline at end of file diff --git a/cura/PrinterOutput/GenericOutputController.py b/cura/PrinterOutput/GenericOutputController.py new file mode 100644 index 0000000000..a21425af92 --- /dev/null +++ b/cura/PrinterOutput/GenericOutputController.py @@ -0,0 +1,151 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from cura.PrinterOutput.PrinterOutputController import PrinterOutputController +from PyQt5.QtCore import QTimer + +MYPY = False +if MYPY: + from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel + from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + + +class GenericOutputController(PrinterOutputController): + def __init__(self, output_device): + super().__init__(output_device) + + self._preheat_bed_timer = QTimer() + self._preheat_bed_timer.setSingleShot(True) + self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) + self._preheat_printer = None + + self._preheat_hotends_timer = QTimer() + self._preheat_hotends_timer.setSingleShot(True) + self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished) + self._preheat_hotends = set() + + self._output_device.printersChanged.connect(self._onPrintersChanged) + self._active_printer = None + + def _onPrintersChanged(self): + if self._active_printer: + self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged) + self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged) + for extruder in self._active_printer.extruders: + extruder.targetHotendTemperatureChanged.disconnect(self._onTargetHotendTemperatureChanged) + + self._active_printer = self._output_device.activePrinter + if self._active_printer: + self._active_printer.stateChanged.connect(self._onPrinterStateChanged) + self._active_printer.targetBedTemperatureChanged.connect(self._onTargetBedTemperatureChanged) + for extruder in self._active_printer.extruders: + extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged) + + def _onPrinterStateChanged(self): + if self._active_printer.state != "idle": + if self._preheat_bed_timer.isActive(): + self._preheat_bed_timer.stop() + self._preheat_printer.updateIsPreheating(False) + if self._preheat_hotends_timer.isActive(): + self._preheat_hotends_timer.stop() + for extruder in self._preheat_hotends: + extruder.updateIsPreheating(False) + self._preheat_hotends = set() + + def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): + self._output_device.sendCommand("G91") + self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) + self._output_device.sendCommand("G90") + + def homeHead(self, printer): + self._output_device.sendCommand("G28 X") + self._output_device.sendCommand("G28 Y") + + def homeBed(self, printer): + self._output_device.sendCommand("G28 Z") + + def setJobState(self, job: "PrintJobOutputModel", state: str): + if state == "pause": + self._output_device.pausePrint() + job.updateState("paused") + elif state == "print": + self._output_device.resumePrint() + job.updateState("printing") + elif state == "abort": + self._output_device.cancelPrint() + pass + + def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): + self._output_device.sendCommand("M140 S%s" % temperature) + + def _onTargetBedTemperatureChanged(self): + if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0: + self._preheat_bed_timer.stop() + self._preheat_printer.updateIsPreheating(False) + + def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): + try: + temperature = round(temperature) # The API doesn't allow floating point. + duration = round(duration) + except ValueError: + return # Got invalid values, can't pre-heat. + + self.setTargetBedTemperature(printer, temperature=temperature) + self._preheat_bed_timer.setInterval(duration * 1000) + self._preheat_bed_timer.start() + self._preheat_printer = printer + printer.updateIsPreheating(True) + + def cancelPreheatBed(self, printer: "PrinterOutputModel"): + self.setTargetBedTemperature(printer, temperature=0) + self._preheat_bed_timer.stop() + printer.updateIsPreheating(False) + + def _onPreheatBedTimerFinished(self): + self.setTargetBedTemperature(self._preheat_printer, 0) + self._preheat_printer.updateIsPreheating(False) + + def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int): + self._output_device.sendCommand("M104 S%s T%s" % (temperature, position)) + + def _onTargetHotendTemperatureChanged(self): + if not self._preheat_hotends_timer.isActive(): + return + + for extruder in self._active_printer.extruders: + if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0: + extruder.updateIsPreheating(False) + self._preheat_hotends.remove(extruder) + if not self._preheat_hotends: + self._preheat_hotends_timer.stop() + + def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): + position = extruder.getPosition() + number_of_extruders = len(extruder.getPrinter().extruders) + if position >= number_of_extruders: + return # Got invalid extruder nr, can't pre-heat. + + try: + temperature = round(temperature) # The API doesn't allow floating point. + duration = round(duration) + except ValueError: + return # Got invalid values, can't pre-heat. + + self.setTargetHotendTemperature(extruder.getPrinter(), position, temperature=temperature) + self._preheat_hotends_timer.setInterval(duration * 1000) + self._preheat_hotends_timer.start() + self._preheat_hotends.add(extruder) + extruder.updateIsPreheating(True) + + def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): + self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0) + if extruder in self._preheat_hotends: + extruder.updateIsPreheating(False) + self._preheat_hotends.remove(extruder) + if not self._preheat_hotends and self._preheat_hotends_timer.isActive(): + self._preheat_hotends_timer.stop() + + def _onPreheatHotendsTimerFinished(self): + for extruder in self._preheat_hotends: + self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) + self._preheat_hotends = set() diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index eefbd9ae12..1537d51919 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -3,6 +3,8 @@ from UM.Application import Application from UM.Logger import Logger +from UM.Settings.ContainerRegistry import ContainerRegistry +from cura.CuraApplication import CuraApplication from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState @@ -254,6 +256,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_manager_create_time = time() self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + machine_manager = CuraApplication.getInstance().getMachineManager() + machine_manager.checkCorrectGroupName(self.getId(), self.name) + def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 1d658e79be..b5b0dd0be3 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -15,6 +15,7 @@ class PrinterOutputController: self.can_pause = True self.can_abort = True self.can_pre_heat_bed = True + self.can_pre_heat_hotends = True self.can_control_manually = True self._output_device = output_device @@ -33,6 +34,12 @@ class PrinterOutputController: def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): Logger.log("w", "Preheat bed not implemented in controller") + def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): + Logger.log("w", "Cancel preheat hotend not implemented in controller") + + def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): + Logger.log("w", "Preheat hotend not implemented in controller") + def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed): Logger.log("w", "Set head position not implemented in controller") diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 712f9b5b1e..e936ace196 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -238,6 +238,13 @@ class PrinterOutputModel(QObject): return self._controller.can_pre_heat_bed return False + # Does the printer support pre-heating the bed at all + @pyqtProperty(bool, constant=True) + def canPreHeatHotends(self): + if self._controller: + return self._controller.can_pre_heat_hotends + return False + # Does the printer support pause at all @pyqtProperty(bool, constant=True) def canPause(self): diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index 3a563c2764..66bc8a7fc3 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -1,6 +1,8 @@ # Copyright (c) 2016 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from PyQt5.QtCore import QTimer + from UM.Application import Application from UM.Math.Polygon import Polygon from UM.Scene.SceneNodeDecorator import SceneNodeDecorator @@ -22,6 +24,10 @@ class ConvexHullDecorator(SceneNodeDecorator): self._global_stack = None + # Make sure the timer is created on the main thread + self._recompute_convex_hull_timer = None + Application.getInstance().callLater(self.createRecomputeConvexHullTimer) + self._raft_thickness = 0.0 # For raft thickness, DRY self._build_volume = Application.getInstance().getBuildVolume() @@ -33,6 +39,12 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onGlobalStackChanged() + def createRecomputeConvexHullTimer(self): + self._recompute_convex_hull_timer = QTimer() + self._recompute_convex_hull_timer.setInterval(200) + self._recompute_convex_hull_timer.setSingleShot(True) + self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull) + def setNode(self, node): previous_node = self._node # Disconnect from previous node signals @@ -99,6 +111,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() return None + def recomputeConvexHullDelayed(self): + if self._recompute_convex_hull_timer is not None: + self._recompute_convex_hull_timer.start() + else: + self.recomputeConvexHull() + def recomputeConvexHull(self): controller = Application.getInstance().getController() root = controller.getScene().getRoot() @@ -279,7 +297,8 @@ class ConvexHullDecorator(SceneNodeDecorator): def _onChanged(self, *args): self._raft_thickness = self._build_volume.getRaftThickness() - self.recomputeConvexHull() + if not args or args[0] == self._node: + self.recomputeConvexHullDelayed() def _onGlobalStackChanged(self): if self._global_stack: diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 7161169b22..760a288b7b 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -348,15 +348,18 @@ class ContainerManager(QObject): # # \param material_id \type{str} the id of the material for which to get the linked materials. # \return \type{list} a list of names of materials with the same GUID - @pyqtSlot("QVariant", result = "QStringList") - def getLinkedMaterials(self, material_node): + @pyqtSlot("QVariant", bool, result = "QStringList") + def getLinkedMaterials(self, material_node, exclude_self = False): guid = material_node.metadata["GUID"] + self_root_material_id = material_node.metadata["base_file"] material_group_list = self._material_manager.getMaterialGroupListByGUID(guid) linked_material_names = [] if material_group_list: for material_group in material_group_list: + if exclude_self and material_group.name == self_root_material_id: + continue linked_material_names.append(material_group.root_material_node.metadata["name"]) return linked_material_names diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 0cf1c7399f..ab48eaddd2 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -173,12 +173,13 @@ class CuraContainerRegistry(ContainerRegistry): plugin_registry = PluginRegistry.getInstance() extension = file_name.split(".")[-1] - global_container_stack = Application.getInstance().getGlobalContainerStack() - if not global_container_stack: + global_stack = Application.getInstance().getGlobalContainerStack() + if not global_stack: return - machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId())) - machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position")) + machine_extruders = [] + for position in sorted(global_stack.extruders): + machine_extruders.append(global_stack.extruders[position]) for plugin_id, meta_data in self._getIOPlugins("profile_reader"): if meta_data["profile_reader"][0]["extension"] != extension: @@ -200,13 +201,18 @@ class CuraContainerRegistry(ContainerRegistry): # First check if this profile is suitable for this machine global_profile = None + extruder_profiles = [] if len(profile_or_list) == 1: global_profile = profile_or_list[0] else: for profile in profile_or_list: if not profile.getMetaDataEntry("position"): global_profile = profile - break + else: + extruder_profiles.append(profile) + extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) + profile_or_list = [global_profile] + extruder_profiles + if not global_profile: Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) return { "status": "error", @@ -227,7 +233,7 @@ class CuraContainerRegistry(ContainerRegistry): # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition) - expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_container_stack.definition) + expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition) # And check if the profile_definition matches either one (showing error if not): if profile_definition != expected_machine_definition: @@ -251,8 +257,8 @@ class CuraContainerRegistry(ContainerRegistry): if len(profile_or_list) == 1: global_profile = profile_or_list[0] extruder_profiles = [] - for idx, extruder in enumerate(global_container_stack.extruders.values()): - profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1)) + for idx, extruder in enumerate(global_stack.extruders.values()): + profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) profile = InstanceContainer(profile_id) profile.setName(quality_name) profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) @@ -264,12 +270,12 @@ class CuraContainerRegistry(ContainerRegistry): if idx == 0: # move all per-extruder settings to the first extruder's quality_changes for qc_setting_key in global_profile.getAllKeys(): - settable_per_extruder = global_container_stack.getProperty(qc_setting_key, + settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") if settable_per_extruder: setting_value = global_profile.getProperty(qc_setting_key, "value") - setting_definition = global_container_stack.getSettingDefinition(qc_setting_key) + setting_definition = global_stack.getSettingDefinition(qc_setting_key) new_instance = SettingInstance(setting_definition, profile) new_instance.setProperty("value", setting_value) new_instance.resetState() # Ensure that the state is not seen as a user state. @@ -286,7 +292,7 @@ class CuraContainerRegistry(ContainerRegistry): for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: # This is assumed to be the global profile - profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") + profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") elif profile_index < len(machine_extruders) + 1: # This is assumed to be an extruder profile diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py index 4ee5ab3c3b..f179dabd5a 100644 --- a/cura/Settings/ExtrudersModel.py +++ b/cura/Settings/ExtrudersModel.py @@ -210,6 +210,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): item = { "id": "", "name": catalog.i18nc("@menuitem", "Not overridden"), + "enabled": True, "color": "#ffffff", "index": -1, "definition": "" diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 3af6f70e5f..07548e21dd 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -10,7 +10,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Signal import Signal from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer -import UM.FlameProfiler from UM.FlameProfiler import pyqtSlot from UM import Util @@ -24,7 +23,6 @@ from UM.Settings.SettingFunction import SettingFunction from UM.Signal import postponeSignals, CompressTechnique from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -from cura.Machines.VariantManager import VariantType from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel @@ -147,6 +145,7 @@ class MachineManager(QObject): activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed. activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed + numberExtrudersEnabledChanged = pyqtSignal() # Emitted when the number of extruders that are enabled changed blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly @@ -467,16 +466,16 @@ class MachineManager(QObject): return self._global_container_stack.getId() return "" - @pyqtProperty(str, notify = globalContainerChanged) + @pyqtProperty(str, notify = outputDevicesChanged) def activeMachineNetworkKey(self) -> str: if self._global_container_stack: - return self._global_container_stack.getMetaDataEntry("um_network_key") + return self._global_container_stack.getMetaDataEntry("um_network_key", "") return "" - @pyqtProperty(str, notify = globalContainerChanged) + @pyqtProperty(str, notify = outputDevicesChanged) def activeMachineNetworkGroupName(self) -> str: if self._global_container_stack: - return self._global_container_stack.getMetaDataEntry("connect_group_name") + return self._global_container_stack.getMetaDataEntry("connect_group_name", "") return "" @pyqtProperty(QObject, notify = globalContainerChanged) @@ -662,12 +661,22 @@ class MachineManager(QObject): if other_machine_stacks: self.setActiveMachine(other_machine_stacks[0]["id"]) + metadata = ContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] + network_key = metadata["um_network_key"] if "um_network_key" in metadata else None ExtruderManager.getInstance().removeMachineExtruders(machine_id) containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) for container in containers: ContainerRegistry.getInstance().removeContainer(container["id"]) ContainerRegistry.getInstance().removeContainer(machine_id) + # If the printer that is being removed is a network printer, the hidden printers have to be also removed + if network_key: + metadata_filter = {"um_network_key": network_key} + hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + if hidden_containers: + # This reuses the method and remove all printers recursively + self.removeMachine(hidden_containers[0].getId()) + @pyqtProperty(bool, notify = globalContainerChanged) def hasMaterials(self) -> bool: if self._global_container_stack: @@ -729,22 +738,6 @@ class MachineManager(QObject): return result - ## Property to indicate if a machine has "specialized" material profiles. - # Some machines have their own material profiles that "override" the default catch all profiles. - @pyqtProperty(bool, notify = globalContainerChanged) - def filterMaterialsByMachine(self) -> bool: - if self._global_container_stack: - return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False)) - return False - - ## Property to indicate if a machine has "specialized" quality profiles. - # Some machines have their own quality profiles that "override" the default catch all profiles. - @pyqtProperty(bool, notify = globalContainerChanged) - def filterQualityByMachine(self) -> bool: - if self._global_container_stack: - return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False)) - return False - ## Get the Definition ID of a machine (specified by ID) # \param machine_id string machine id to get the definition ID of # \returns DefinitionID (string) if found, None otherwise @@ -872,7 +865,13 @@ class MachineManager(QObject): for position, extruder in self._global_container_stack.extruders.items(): if extruder.isEnabled: extruder_count += 1 - definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) + if self.numberExtrudersEnabled != extruder_count: + definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) + self.numberExtrudersEnabledChanged.emit() + + @pyqtProperty(int, notify = numberExtrudersEnabledChanged) + def numberExtrudersEnabled(self): + return self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") @pyqtProperty(str, notify = extruderChanged) def defaultExtruderPosition(self): @@ -1110,7 +1109,7 @@ class MachineManager(QObject): from cura.Settings.CuraContainerStack import _ContainerIndexes context = PropertyEvaluationContext(extruder) context.context["evaluate_from_container_index"] = _ContainerIndexes.DefinitionChanges - material_diameter = self._global_container_stack.getProperty("material_diameter", "value", context) + material_diameter = extruder.getProperty("material_diameter", "value", context) candidate_materials = self._material_manager.getAvailableMaterials( self._global_container_stack.definition.getId(), current_variant_name, @@ -1193,6 +1192,24 @@ class MachineManager(QObject): if machine.getMetaDataEntry(key) == value: machine.setMetaDataEntry(key, new_value) + ## This method checks if the name of the group stored in the definition container is correct. + # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group + # then all the container stacks are updated, both the current and the hidden ones. + def checkCorrectGroupName(self, device_id: str, group_name: str): + if self._global_container_stack and device_id == self.activeMachineNetworkKey: + # Check if the connect_group_name is correct. If not, update all the containers connected to the same printer + if self.activeMachineNetworkGroupName != group_name: + metadata_filter = {"um_network_key": self.activeMachineNetworkKey} + hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + for container in hidden_containers: + container.setMetaDataEntry("connect_group_name", group_name) + + ## This method checks if there is an instance connected to the given network_key + def existNetworkInstances(self, network_key: str) -> bool: + metadata_filter = {"um_network_key": network_key} + containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + return bool(containers) + @pyqtSlot("QVariant") def setGlobalVariant(self, container_node): self.blurSettings.emit() @@ -1242,6 +1259,13 @@ class MachineManager(QObject): if not no_dialog and self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: self._application.discardOrKeepProfileChanges() + @pyqtSlot() + def resetToUseDefaultQuality(self): + with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): + self._setQualityGroup(self._current_quality_group) + for stack in [self._global_container_stack] + list(self._global_container_stack.extruders.values()): + stack.userChanges.clear() + @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) def activeQualityChangesGroup(self): return self._current_quality_changes_group diff --git a/cura/Settings/SettingVisibilityPresetsModel.py b/cura/Settings/SettingVisibilityPresetsModel.py deleted file mode 100644 index e5a2e24412..0000000000 --- a/cura/Settings/SettingVisibilityPresetsModel.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -import urllib -from configparser import ConfigParser - -from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl - -from UM.Logger import Logger -from UM.Qt.ListModel import ListModel -from UM.Preferences import Preferences -from UM.Resources import Resources -from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError - -import cura.CuraApplication - - -class SettingVisibilityPresetsModel(ListModel): - IdRole = Qt.UserRole + 1 - NameRole = Qt.UserRole + 2 - SettingsRole = Qt.UserRole + 4 - - def __init__(self, parent = None): - super().__init__(parent) - self.addRoleName(self.IdRole, "id") - self.addRoleName(self.NameRole, "name") - self.addRoleName(self.SettingsRole, "settings") - - self._populate() - - self._preferences = Preferences.getInstance() - self._preferences.addPreference("cura/active_setting_visibility_preset", "custom") # Preference to store which preset is currently selected - self._preferences.addPreference("cura/custom_visible_settings", "") # Preference that stores the "custom" set so it can always be restored (even after a restart) - self._preferences.preferenceChanged.connect(self._onPreferencesChanged) - - self._active_preset = self._preferences.getValue("cura/active_setting_visibility_preset") - if self.find("id", self._active_preset) < 0: - self._active_preset = "custom" - - self.activePresetChanged.emit() - - - def _populate(self): - items = [] - for item in Resources.getAllResourcesOfType(cura.CuraApplication.CuraApplication.ResourceTypes.SettingVisibilityPreset): - try: - mime_type = MimeTypeDatabase.getMimeTypeForFile(item) - except MimeTypeNotFoundError: - Logger.log("e", "Could not determine mime type of file %s", item) - continue - - id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(item))) - - if not os.path.isfile(item): - continue - - parser = ConfigParser(allow_no_value=True) # accept options without any value, - - try: - parser.read([item]) - - if not parser.has_option("general", "name") and not parser.has_option("general", "weight"): - continue - - settings = [] - for section in parser.sections(): - if section == 'general': - continue - - settings.append(section) - for option in parser[section].keys(): - settings.append(option) - - items.append({ - "id": id, - "name": parser["general"]["name"], - "weight": parser["general"]["weight"], - "settings": settings - }) - - except Exception as e: - Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e)) - - - items.sort(key = lambda k: (k["weight"], k["id"])) - self.setItems(items) - - @pyqtSlot(str) - def setActivePreset(self, preset_id): - if preset_id != "custom" and self.find("id", preset_id) == -1: - Logger.log("w", "Tried to set active preset to unknown id %s", preset_id) - return - - if preset_id == "custom" and self._active_preset == "custom": - # Copy current visibility set to custom visibility set preference so it can be restored later - visibility_string = self._preferences.getValue("general/visible_settings") - self._preferences.setValue("cura/custom_visible_settings", visibility_string) - - self._preferences.setValue("cura/active_setting_visibility_preset", preset_id) - - self._active_preset = preset_id - self.activePresetChanged.emit() - - activePresetChanged = pyqtSignal() - - @pyqtProperty(str, notify = activePresetChanged) - def activePreset(self): - return self._active_preset - - def _onPreferencesChanged(self, name): - if name != "general/visible_settings": - return - - if self._active_preset != "custom": - return - - # Copy current visibility set to custom visibility set preference so it can be restored later - visibility_string = self._preferences.getValue("general/visible_settings") - self._preferences.setValue("cura/custom_visible_settings", visibility_string) - - - # Factory function, used by QML - @staticmethod - def createSettingVisibilityPresetsModel(engine, js_engine): - return SettingVisibilityPresetsModel.getInstance() - - ## Get the singleton instance for this class. - @classmethod - def getInstance(cls) -> "SettingVisibilityPresetsModel": - # Note: Explicit use of class name to prevent issues with inheritance. - if not SettingVisibilityPresetsModel.__instance: - SettingVisibilityPresetsModel.__instance = cls() - return SettingVisibilityPresetsModel.__instance - - __instance = None # type: "SettingVisibilityPresetsModel" \ No newline at end of file diff --git a/cura/Settings/SimpleModeSettingsManager.py b/cura/Settings/SimpleModeSettingsManager.py index 867a21702c..a337d8b04e 100644 --- a/cura/Settings/SimpleModeSettingsManager.py +++ b/cura/Settings/SimpleModeSettingsManager.py @@ -16,7 +16,8 @@ class SimpleModeSettingsManager(QObject): self._is_profile_user_created = False # True when profile was custom created by user self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized) - self._machine_manager.activeQualityChanged.connect(self._updateIsProfileUserCreated) + self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated) + self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated) # update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts self._updateIsProfileCustomized() diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index f5daa77bb0..214623c92d 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -280,6 +280,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check if any quality_changes instance container is in conflict. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] quality_name = "" + custom_quality_name = "" num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes num_user_settings = 0 quality_changes_conflict = False @@ -292,7 +293,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_id = self._stripFileToId(instance_container_file_name) serialized = archive.open(instance_container_file_name).read().decode("utf-8") - serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name) + + # Qualities and variants don't have upgrades, so don't upgrade them + parser = ConfigParser(interpolation = None) + parser.read_string(serialized) + container_type = parser["metadata"]["type"] + if container_type not in ("quality", "variant"): + serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name) + parser = ConfigParser(interpolation = None) parser.read_string(serialized) container_info = ContainerInfo(instance_container_file_name, serialized, parser) @@ -309,7 +317,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): position = parser["metadata"]["position"] self._machine_info.quality_changes_info.extruder_info_dict[position] = container_info - quality_name = parser["general"]["name"] + custom_quality_name = parser["general"]["name"] values = parser["values"] if parser.has_section("values") else dict() num_settings_overriden_by_quality_changes += len(values) # Check if quality changes already exists. @@ -473,6 +481,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruders = num_extruders * [""] + quality_name = custom_quality_name if custom_quality_name else quality_name + self._machine_info.container_id = global_stack_id self._machine_info.name = machine_name self._machine_info.definition_id = machine_definition_id diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 1b3b7264a1..ccd881afdc 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -1,17 +1,17 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import re # For escaping characters in the settings. +import json +import copy + from UM.Mesh.MeshWriter import MeshWriter from UM.Logger import Logger from UM.Application import Application from UM.Settings.InstanceContainer import InstanceContainer -from UM.Util import parseBool -from cura.Settings.ExtruderManager import ExtruderManager +from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -import re #For escaping characters in the settings. -import json -import copy ## Writes g-code to a file. # @@ -45,6 +45,8 @@ class GCodeWriter(MeshWriter): def __init__(self): super().__init__() + self._application = Application.getInstance() + ## Writes the g-code for the entire scene to a stream. # # Note that even though the function accepts a collection of nodes, the @@ -94,7 +96,6 @@ class GCodeWriter(MeshWriter): return flat_container - ## Serialises a container stack to prepare it for writing at the end of the # g-code. # @@ -104,15 +105,20 @@ class GCodeWriter(MeshWriter): # \param settings A container stack to serialise. # \return A serialised string of the settings. def _serialiseSettings(self, stack): + container_registry = self._application.getContainerRegistry() + quality_manager = self._application.getQualityManager() + prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line. prefix_length = len(prefix) + quality_type = stack.quality.getMetaDataEntry("quality_type") container_with_profile = stack.qualityChanges if container_with_profile.getId() == "empty_quality_changes": - Logger.log("e", "No valid quality profile found, not writing settings to g-code!") - return "" + # If the global quality changes is empty, create a new one + quality_name = container_registry.uniqueName(stack.quality.getName()) + container_with_profile = quality_manager._createQualityChanges(quality_type, quality_name, stack, None) - flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile) + flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile) # If the quality changes is not set, we need to set type manually if flat_global_container.getMetaDataEntry("type", None) is None: flat_global_container.addMetaDataEntry("type", "quality_changes") @@ -121,41 +127,47 @@ class GCodeWriter(MeshWriter): if flat_global_container.getMetaDataEntry("quality_type", None) is None: flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal")) - # Change the default defintion - default_machine_definition = "fdmprinter" - if parseBool(stack.getMetaDataEntry("has_machine_quality", "False")): - default_machine_definition = stack.getMetaDataEntry("quality_definition") - if not default_machine_definition: - default_machine_definition = stack.definition.getId() - flat_global_container.setMetaDataEntry("definition", default_machine_definition) + # Get the machine definition ID for quality profiles + machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(stack.definition) + flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality) serialized = flat_global_container.serialize() data = {"global_quality": serialized} - for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")): + all_setting_keys = set(flat_global_container.getAllKeys()) + for extruder in sorted(stack.extruders.values(), key = lambda k: int(k.getMetaDataEntry("position"))): extruder_quality = extruder.qualityChanges if extruder_quality.getId() == "empty_quality_changes": - Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId()) - continue - flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality) + # Same story, if quality changes is empty, create a new one + quality_name = container_registry.uniqueName(stack.quality.getName()) + extruder_quality = quality_manager._createQualityChanges(quality_type, quality_name, stack, None) + + flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality) # If the quality changes is not set, we need to set type manually if flat_extruder_quality.getMetaDataEntry("type", None) is None: flat_extruder_quality.addMetaDataEntry("type", "quality_changes") # Ensure that extruder is set. (Can happen if we have empty quality changes). - if flat_extruder_quality.getMetaDataEntry("extruder", None) is None: - flat_extruder_quality.addMetaDataEntry("extruder", extruder.getBottom().getId()) + if flat_extruder_quality.getMetaDataEntry("position", None) is None: + flat_extruder_quality.addMetaDataEntry("position", extruder.getMetaDataEntry("position")) # Ensure that quality_type is set. (Can happen if we have empty quality changes). if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None: flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal")) - # Change the default defintion - flat_extruder_quality.setMetaDataEntry("definition", default_machine_definition) + # Change the default definition + flat_extruder_quality.setMetaDataEntry("definition", machine_definition_id_for_quality) extruder_serialized = flat_extruder_quality.serialize() data.setdefault("extruder_quality", []).append(extruder_serialized) + all_setting_keys.update(set(flat_extruder_quality.getAllKeys())) + + # Check if there is any profiles + if not all_setting_keys: + Logger.log("i", "No custom settings found, not writing settings to g-code.") + return "" + json_string = json.dumps(data) # Escape characters that have a special meaning in g-code comments. @@ -169,5 +181,5 @@ class GCodeWriter(MeshWriter): # Lines have 80 characters, so the payload of each line is 80 - prefix. for pos in range(0, len(escaped_string), 80 - prefix_length): - result += prefix + escaped_string[pos : pos + 80 - prefix_length] + "\n" + result += prefix + escaped_string[pos: pos + 80 - prefix_length] + "\n" return result diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 03a2ce1bf4..a2790dcf08 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -163,7 +163,16 @@ Item { id: addedSettingsModel; containerId: Cura.MachineManager.activeDefinitionId expanded: [ "*" ] - exclude: { + filter: + { + if (printSequencePropertyProvider.properties.value == "one_at_a_time") + { + return {"settable_per_meshgroup": true}; + } + return {"settable_per_mesh": true}; + } + exclude: + { var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]; if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh") @@ -375,7 +384,6 @@ Item { title: catalog.i18nc("@title:window", "Select Settings to Customize for this model") width: screenScaleFactor * 360 - property string labelFilter: "" property var additional_excluded_settings onVisibilityChanged: @@ -386,11 +394,33 @@ Item { // Set skip setting, it will prevent from resetting selected mesh_type contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) listview.model.forceUpdate() + + updateFilter() } } + function updateFilter() + { + var new_filter = {}; + if (printSequencePropertyProvider.properties.value == "one_at_a_time") + { + new_filter["settable_per_meshgroup"] = true; + } + else + { + new_filter["settable_per_mesh"] = true; + } + + if(filterInput.text != "") + { + new_filter["i18n_label"] = "*" + filterInput.text; + } + + listview.model.filter = new_filter; + } + TextField { - id: filter + id: filterInput anchors { top: parent.top @@ -401,17 +431,7 @@ Item { placeholderText: catalog.i18nc("@label:textbox", "Filter..."); - onTextChanged: - { - if(text != "") - { - listview.model.filter = {"settable_per_mesh": true, "i18n_label": "*" + text} - } - else - { - listview.model.filter = {"settable_per_mesh": true} - } - } + onTextChanged: settingPickDialog.updateFilter() } CheckBox @@ -437,7 +457,7 @@ Item { anchors { - top: filter.bottom; + top: filterInput.bottom; left: parent.left; right: parent.right; bottom: parent.bottom; @@ -449,10 +469,6 @@ Item { { id: definitionsModel; containerId: Cura.MachineManager.activeDefinitionId - filter: - { - "settable_per_mesh": true - } visibilityHandler: UM.SettingPreferenceVisibilityHandler {} expanded: [ "*" ] exclude: @@ -484,6 +500,7 @@ Item { } } } + Component.onCompleted: settingPickDialog.updateFilter() } } @@ -507,6 +524,16 @@ Item { storeIndex: 0 } + UM.SettingPropertyProvider + { + id: printSequencePropertyProvider + + containerStackId: Cura.MachineManager.activeMachineId + key: "print_sequence" + watchedProperties: [ "value" ] + storeIndex: 0 + } + SystemPalette { id: palette; } Component diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index ff930e2c31..c81e4a76bc 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os.path @@ -7,7 +7,7 @@ from UM.Application import Application from UM.Logger import Logger from UM.Message import Message from UM.FileHandler.WriteFileJob import WriteFileJob -from UM.Mesh.MeshWriter import MeshWriter +from UM.FileHandler.FileWriter import FileWriter #To check against the write modes (text vs. binary). from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice import OutputDeviceError @@ -39,7 +39,7 @@ class RemovableDriveOutputDevice(OutputDevice): # MIME types available to the currently active machine? # def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): - filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do) + filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() @@ -56,19 +56,21 @@ class RemovableDriveOutputDevice(OutputDevice): machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")] # Take the intersection between file_formats and machine_file_formats. - file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats)) + format_by_mimetype = {format["mime_type"]: format for format in file_formats} + file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") - raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("There are no file formats available to write with!")) + raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "There are no file formats available to write with!")) + preferred_format = file_formats[0] # Just take the first file format available. if file_handler is not None: - writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"]) + writer = file_handler.getWriterByMimeType(preferred_format["mime_type"]) else: - writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) + writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"]) - extension = file_formats[0]["extension"] + extension = preferred_format["extension"] if file_name is None: file_name = self._automaticFileName(nodes) @@ -80,8 +82,11 @@ class RemovableDriveOutputDevice(OutputDevice): try: Logger.log("d", "Writing to %s", file_name) # Using buffering greatly reduces the write time for many lines of gcode - self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") - job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) + if preferred_format["mode"] == FileWriter.OutputMode.TextMode: + self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") + else: #Binary mode. + self._stream = open(file_name, "wb", buffering = 1) + job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"]) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 35ce9cc37a..456d64e250 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -158,9 +158,12 @@ class SimulationView(View): return self._nozzle_node def _onSceneChanged(self, node): - self.setActivity(False) - self.calculateMaxLayers() - self.calculateMaxPathsOnLayer(self._current_layer_num) + if node.getMeshData() is None: + self.resetLayerData() + else: + self.setActivity(False) + self.calculateMaxLayers() + self.calculateMaxPathsOnLayer(self._current_layer_num) def isBusy(self): return self._busy diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8b3ad0f4dd..3332b4181d 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,39 +1,103 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Math.Vector import Vector -from UM.Tool import Tool -from PyQt5.QtCore import Qt, QUrl -from UM.Application import Application -from UM.Event import Event -from UM.Mesh.MeshBuilder import MeshBuilder -from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation -from UM.Settings.SettingInstance import SettingInstance -from cura.Scene.CuraSceneNode import CuraSceneNode -from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator -from cura.Scene.BuildPlateDecorator import BuildPlateDecorator -from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator import os import os.path +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtWidgets import QApplication + +from UM.Math.Vector import Vector +from UM.Tool import Tool +from UM.Application import Application +from UM.Event import Event, MouseEvent + +from UM.Mesh.MeshBuilder import MeshBuilder +from UM.Scene.Selection import Selection +from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator +from cura.Scene.CuraSceneNode import CuraSceneNode + +from cura.PickingPass import PickingPass + +from UM.Operations.GroupedOperation import GroupedOperation +from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation +from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation +from cura.Operations.SetParentOperation import SetParentOperation + +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from UM.Scene.GroupDecorator import GroupDecorator +from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator + +from UM.Settings.SettingInstance import SettingInstance + class SupportEraser(Tool): def __init__(self): super().__init__() self._shortcut_key = Qt.Key_G - self._controller = Application.getInstance().getController() + self._controller = self.getController() + + self._selection_pass = None + Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) + + # Note: if the selection is cleared with this tool active, there is no way to switch to + # another tool than to reselect an object (by clicking it) because the tool buttons in the + # toolbar will have been disabled. That is why we need to ignore the first press event + # after the selection has been cleared. + Selection.selectionChanged.connect(self._onSelectionChanged) + self._had_selection = False + self._skip_press = False + + self._had_selection_timer = QTimer() + self._had_selection_timer.setInterval(0) + self._had_selection_timer.setSingleShot(True) + self._had_selection_timer.timeout.connect(self._selectionChangeDelay) def event(self, event): super().event(event) + modifiers = QApplication.keyboardModifiers() + ctrl_is_active = modifiers & Qt.ControlModifier - if event.type == Event.ToolActivateEvent: + if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + if ctrl_is_active: + self._controller.setActiveTool("TranslateTool") + return - # Load the remover mesh: - self._createEraserMesh() + if self._skip_press: + # The selection was previously cleared, do not add/remove an anti-support mesh but + # use this click for selection and reactivating this tool only. + self._skip_press = False + return - # After we load the mesh, deactivate the tool again: - self.getController().setActiveTool(None) + if self._selection_pass is None: + # The selection renderpass is used to identify objects in the current view + self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection") + picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y)) + if not picked_node: + # There is no slicable object at the picked location + return - def _createEraserMesh(self): + node_stack = picked_node.callDecoration("getStack") + if node_stack: + if node_stack.getProperty("anti_overhang_mesh", "value"): + self._removeEraserMesh(picked_node) + return + + elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"): + # Only "normal" meshes can have anti_overhang_meshes added to them + return + + # Create a pass for picking a world-space location from the mouse location + active_camera = self._controller.getScene().getActiveCamera() + picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) + picking_pass.render() + + picked_position = picking_pass.getPickedPosition(event.x, event.y) + + # Add the anti_overhang_mesh cube at the picked location + self._createEraserMesh(picked_node, picked_position) + + def _createEraserMesh(self, parent: CuraSceneNode, position: Vector): node = CuraSceneNode() node.setName("Eraser") @@ -41,9 +105,7 @@ class SupportEraser(Tool): mesh = MeshBuilder() mesh.addCube(10,10,10) node.setMeshData(mesh.build()) - # Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF - move_vector = Vector(0, 5, 0) - node.setPosition(move_vector) + node.setPosition(position) active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate @@ -51,21 +113,60 @@ class SupportEraser(Tool): node.addDecorator(BuildPlateDecorator(active_build_plate)) node.addDecorator(SliceableObjectDecorator()) - stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. - if not stack: - node.addDecorator(SettingOverrideDecorator()) - stack = node.callDecoration("getStack") - + stack = node.callDecoration("getStack") # created by SettingOverrideDecorator settings = stack.getTop() - if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")): - definition = stack.getSettingDefinition("anti_overhang_mesh") - new_instance = SettingInstance(definition, settings) - new_instance.setProperty("value", True) - new_instance.resetState() # Ensure that the state is not seen as a user state. - settings.addInstance(new_instance) + definition = stack.getSettingDefinition("anti_overhang_mesh") + new_instance = SettingInstance(definition, settings) + new_instance.setProperty("value", True) + new_instance.resetState() # Ensure that the state is not seen as a user state. + settings.addInstance(new_instance) - scene = self._controller.getScene() - op = AddSceneNodeOperation(node, scene.getRoot()) + root = self._controller.getScene().getRoot() + + op = GroupedOperation() + # First add the node to the scene, so it gets the expected transform + op.addOperation(AddSceneNodeOperation(node, root)) + op.addOperation(SetParentOperation(node, parent)) op.push() + Application.getInstance().getController().getScene().sceneChanged.emit(node) + + def _removeEraserMesh(self, node: CuraSceneNode): + parent = node.getParent() + if parent == self._controller.getScene().getRoot(): + parent = None + + op = RemoveSceneNodeOperation(node) + op.push() + + if parent and not Selection.isSelected(parent): + Selection.add(parent) + + Application.getInstance().getController().getScene().sceneChanged.emit(node) + + def _updateEnabled(self): + plugin_enabled = False + + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack: + plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled") + + Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) + + def _onSelectionChanged(self): + # When selection is passed from one object to another object, first the selection is cleared + # and then it is set to the new object. We are only interested in the change from no selection + # to a selection or vice-versa, not in a change from one object to another. A timer is used to + # "merge" a possible clear/select action in a single frame + if Selection.hasSelection() != self._had_selection: + self._had_selection_timer.start() + + def _selectionChangeDelay(self): + has_selection = Selection.hasSelection() + if not has_selection and self._had_selection: + self._skip_press = True + else: + self._skip_press = False + + self._had_selection = has_selection diff --git a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py index 4615cd62dc..076c4584af 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py @@ -13,6 +13,7 @@ class ClusterUM3PrinterOutputController(PrinterOutputController): def __init__(self, output_device): super().__init__(output_device) self.can_pre_heat_bed = False + self.can_pre_heat_hotends = False self.can_control_manually = False def setJobState(self, job: "PrintJobOutputModel", state: str): diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index 76e8721fdd..0b8d6e9f53 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -147,6 +147,10 @@ class DiscoverUM3Action(MachineAction): return "" + @pyqtSlot(str, result = bool) + def existsKey(self, key) -> bool: + return Application.getInstance().getMachineManager().existNetworkInstances(network_key = key) + @pyqtSlot() def loadConfigurationFromPrinter(self): machine_manager = Application.getInstance().getMachineManager() diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index 079e5dcdd3..0aaeef8fbd 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -5,6 +5,7 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.1 import QtQuick.Window 2.1 +import QtQuick.Dialogs 1.2 Cura.MachineAction { @@ -33,15 +34,33 @@ Cura.MachineAction { var printerKey = base.selectedDevice.key var printerName = base.selectedDevice.name // TODO To change when the groups have a name - if(manager.getStoredKey() != printerKey) + if (manager.getStoredKey() != printerKey) { - manager.setKey(printerKey) - manager.setGroupName(printerName) // TODO To change when the groups have a name - completed() + // Check if there is another instance with the same key + if (!manager.existsKey(printerKey)) + { + manager.setKey(printerKey) + manager.setGroupName(printerName) // TODO To change when the groups have a name + completed() + } + else + { + existingConnectionDialog.open() + } } } } + MessageDialog + { + id: existingConnectionDialog + title: catalog.i18nc("@window:title", "Existing Connection") + icon: StandardIcon.Information + text: catalog.i18nc("@message:text", "This printer/group is already added to Cura. Please select another printer/group.") + standardButtons: StandardButton.Ok + modality: Qt.ApplicationModal + } + Column { anchors.fill: parent; diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 5ff5eb9e3e..089b9038f7 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -82,6 +82,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._zero_conf_browser.cancel() self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed. + for instance_name in list(self._discovered_devices): + self._onRemoveDevice(instance_name) + self._zero_conf = Zeroconf() self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest]) diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py deleted file mode 100644 index f189ed5876..0000000000 --- a/plugins/USBPrinting/USBPrinterOutputController.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from cura.PrinterOutput.PrinterOutputController import PrinterOutputController -from PyQt5.QtCore import QTimer - -MYPY = False -if MYPY: - from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel - from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel - - -class USBPrinterOutputController(PrinterOutputController): - def __init__(self, output_device): - super().__init__(output_device) - - self._preheat_bed_timer = QTimer() - self._preheat_bed_timer.setSingleShot(True) - self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) - self._preheat_printer = None - - def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): - self._output_device.sendCommand("G91") - self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) - self._output_device.sendCommand("G90") - - def homeHead(self, printer): - self._output_device.sendCommand("G28 X") - self._output_device.sendCommand("G28 Y") - - def homeBed(self, printer): - self._output_device.sendCommand("G28 Z") - - def setJobState(self, job: "PrintJobOutputModel", state: str): - if state == "pause": - self._output_device.pausePrint() - job.updateState("paused") - elif state == "print": - self._output_device.resumePrint() - job.updateState("printing") - elif state == "abort": - self._output_device.cancelPrint() - pass - - def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): - try: - temperature = round(temperature) # The API doesn't allow floating point. - duration = round(duration) - except ValueError: - return # Got invalid values, can't pre-heat. - - self.setTargetBedTemperature(printer, temperature=temperature) - self._preheat_bed_timer.setInterval(duration * 1000) - self._preheat_bed_timer.start() - self._preheat_printer = printer - printer.updateIsPreheating(True) - - def cancelPreheatBed(self, printer: "PrinterOutputModel"): - self.preheatBed(printer, temperature=0, duration=0) - self._preheat_bed_timer.stop() - printer.updateIsPreheating(False) - - def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): - self._output_device.sendCommand("M140 S%s" % temperature) - - def _onPreheatBedTimerFinished(self): - self.setTargetBedTemperature(self._preheat_printer, 0) - self._preheat_printer.updateIsPreheating(False) \ No newline at end of file diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 14098b66f8..24feedd628 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -10,9 +10,9 @@ from UM.PluginRegistry import PluginRegistry from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel +from cura.PrinterOutput.GenericOutputController import GenericOutputController from .AutoDetectBaudJob import AutoDetectBaudJob -from .USBPrinterOutputController import USBPrinterOutputController from .avr_isp import stk500v2, intelHex from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty @@ -240,7 +240,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): container_stack = Application.getInstance().getGlobalContainerStack() num_extruders = container_stack.getProperty("machine_extruder_count", "value") # Ensure that a printer is created. - self._printers = [PrinterOutputModel(output_controller=USBPrinterOutputController(self), number_of_extruders=num_extruders)] + self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)] self._printers[0].updateName(container_stack.getName()) self.setConnectionState(ConnectionState.connected) self._update_thread.start() @@ -372,7 +372,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): elapsed_time = int(time() - self._print_start_time) print_job = self._printers[0].activePrintJob if print_job is None: - print_job = PrintJobOutputModel(output_controller = USBPrinterOutputController(self), name= Application.getInstance().getPrintInformation().jobName) + print_job = PrintJobOutputModel(output_controller = GenericOutputController(self), name= Application.getInstance().getPrintInformation().jobName) print_job.updateState("printing") self._printers[0].updateActivePrintJob(print_job) diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py b/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py index a2d78d8d9f..2037a0211d 100644 --- a/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py +++ b/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py @@ -153,6 +153,10 @@ class VersionUpgrade26to27(VersionUpgrade): if new_id is not None: parser.set("containers", key, new_id) + if "6" not in parser["containers"]: + parser["containers"]["6"] = parser["containers"]["5"] + parser["containers"]["5"] = "empty" + for each_section in ("general", "metadata"): if not parser.has_section(each_section): parser.add_section(each_section) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 8b17721794..5ff6838373 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -208,14 +208,9 @@ class XmlMaterialProfile(InstanceContainer): machine_variant_map = {} variant_manager = CuraApplication.getInstance().getVariantManager() - material_manager = CuraApplication.getInstance().getMaterialManager() root_material_id = self.getMetaDataEntry("base_file") # if basefile is self.getId, this is a basefile. - material_group = material_manager.getMaterialGroup(root_material_id) - - all_containers = [] - for node in [material_group.root_material_node] + material_group.derived_material_node_list: - all_containers.append(node.getContainer()) + all_containers = registry.findInstanceContainers(base_file = root_material_id) for container in all_containers: definition_id = container.getMetaDataEntry("definition") @@ -242,7 +237,7 @@ class XmlMaterialProfile(InstanceContainer): for definition_id, container in machine_container_map.items(): definition_id = container.getMetaDataEntry("definition") - definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = definition_id)[0] + definition_metadata = registry.findDefinitionContainersMetadata(id = definition_id)[0] product = definition_id for product_name, product_id_list in product_id_map.items(): diff --git a/resources/definitions/bq_hephestos_xl.def.json b/resources/definitions/bq_hephestos_xl.def.json index 75b756c71e..08be4b8d34 100644 --- a/resources/definitions/bq_hephestos_xl.def.json +++ b/resources/definitions/bq_hephestos_xl.def.json @@ -6,7 +6,7 @@ "visible": true, "manufacturer": "BQ", "author": "BQ", - "file_formats": "text/x-code", + "file_formats": "text/x-gcode", "platform": "bq_hephestos_platform.stl", "platform_offset": [ 0, -82, 0] }, diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 8567dab08b..21ee543333 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -632,6 +632,73 @@ "settable_per_extruder": false, "settable_per_meshgroup": false }, + "machine_steps_per_mm_x": + { + "label": "Steps per Millimeter (X)", + "description": "How many steps of the stepper motor will result in one millimeter of movement in the X direction.", + "type": "int", + "default_value": 50, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_steps_per_mm_y": + { + "label": "Steps per Millimeter (Y)", + "description": "How many steps of the stepper motor will result in one millimeter of movement in the Y direction.", + "type": "int", + "default_value": 50, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_steps_per_mm_z": + { + "label": "Steps per Millimeter (Z)", + "description": "How many steps of the stepper motor will result in one millimeter of movement in the Z direction.", + "type": "int", + "default_value": 50, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_steps_per_mm_e": + { + "label": "Steps per Millimeter (E)", + "description": "How many steps of the stepper motors will result in one millimeter of extrusion.", + "type": "int", + "default_value": 1600, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_endstop_positive_direction_x": + { + "label": "X Endstop in Positive Direction", + "description": "Whether the endstop of the X axis is in the positive direction (high X coordinate) or negative (low X coordinate).", + "type": "bool", + "default_value": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_endstop_positive_direction_y": + { + "label": "Y Endstop in Positive Direction", + "description": "Whether the endstop of the Y axis is in the positive direction (high Y coordinate) or negative (low Y coordinate).", + "type": "bool", + "default_value": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_endstop_positive_direction_z": + { + "label": "Z Endstop in Positive Direction", + "description": "Whether the endstop of the Z axis is in the positive direction (high Z coordinate) or negative (low Z coordinate).", + "type": "bool", + "default_value": true, + "settable_per_mesh": false, + "settable_per_extruder": true + }, "machine_minimum_feedrate": { "label": "Minimum Feedrate", @@ -642,6 +709,16 @@ "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false + }, + "machine_feeder_wheel_diameter": + { + "label": "Feeder Wheel Diameter", + "description": "The diameter of the wheel that drives the material in the feeder.", + "unit": "mm", + "type": "float", + "default_value": 10.0, + "settable_per_mesh": false, + "settable_per_extruder": true } } }, diff --git a/resources/definitions/malyan_m180.def.json b/resources/definitions/malyan_m180.def.json index 5e0a6038dd..11b61328ed 100644 --- a/resources/definitions/malyan_m180.def.json +++ b/resources/definitions/malyan_m180.def.json @@ -25,8 +25,7 @@ "default_value": true }, "machine_nozzle_size": { - "default_value": 0.4, - "minimum_value": "0.001" + "default_value": 0.4 }, "machine_head_with_fans_polygon": { "default_value": [ @@ -36,6 +35,21 @@ [ 18, 35 ] ] }, + "machine_max_feedrate_z": { + "default_value": 400 + }, + "machine_steps_per_mm_x": { + "default_value": 93 + }, + "machine_steps_per_mm_y": { + "default_value": 93 + }, + "machine_steps_per_mm_z": { + "default_value": 1600 + }, + "machine_steps_per_mm_e": { + "default_value": 92 + }, "gantry_height": { "default_value": 55 }, diff --git a/resources/definitions/seemecnc_artemis.def.json b/resources/definitions/seemecnc_artemis.def.json index 88a1b28de6..0b31abfa41 100644 --- a/resources/definitions/seemecnc_artemis.def.json +++ b/resources/definitions/seemecnc_artemis.def.json @@ -25,6 +25,7 @@ "machine_nozzle_size": { "default_value": 0.5 }, "machine_shape": { "default_value": "elliptic" }, "machine_width": { "default_value": 290 }, + "material_diameter": { "default_value": 1.75 }, "relative_extrusion": { "default_value": false }, "retraction_amount": { "default_value": 3.2 }, "retraction_combing": { "default_value": "off" }, diff --git a/resources/definitions/seemecnc_v32.def.json b/resources/definitions/seemecnc_v32.def.json index 5932403bc5..3f46c1540a 100644 --- a/resources/definitions/seemecnc_v32.def.json +++ b/resources/definitions/seemecnc_v32.def.json @@ -25,6 +25,7 @@ "machine_nozzle_size": { "default_value": 0.5 }, "machine_shape": { "default_value": "elliptic" }, "machine_width": { "default_value": 265 }, + "material_diameter": { "default_value": 1.75 }, "relative_extrusion": { "default_value": false }, "retraction_amount": { "default_value": 3.2 }, "retraction_combing": { "default_value": "off" }, diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 57cfbe960f..ef41686752 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -110,9 +110,9 @@ "material_bed_temperature": { "maximum_value": "115" }, "material_bed_temperature_layer_0": { "maximum_value": "115" }, "material_standby_temperature": { "value": "100" }, - "meshfix_maximum_resolution": { "value": "0.04" }, + "meshfix_maximum_resolution": { "value": "0.04" }, "multiple_mesh_overlap": { "value": "0" }, - "optimize_wall_printing_order": { "value": "True" }, + "optimize_wall_printing_order": { "value": "True" }, "prime_tower_enable": { "default_value": true }, "raft_airgap": { "value": "0" }, "raft_base_thickness": { "value": "0.3" }, diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index cff4399073..c4ebb790e8 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -217,6 +217,7 @@ UM.MainWindow text: catalog.i18nc("@action:inmenu", "Disable Extruder") onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) visible: Cura.MachineManager.getExtruder(model.index).isEnabled + enabled: Cura.MachineManager.numberExtrudersEnabled > 1 } } diff --git a/resources/qml/MachineSelection.qml b/resources/qml/MachineSelection.qml index b3f9629703..d075486eb2 100644 --- a/resources/qml/MachineSelection.qml +++ b/resources/qml/MachineSelection.qml @@ -10,17 +10,22 @@ import UM 1.2 as UM import Cura 1.0 as Cura import "Menus" -ToolButton { +ToolButton +{ id: base property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property var printerStatus: Cura.MachineManager.printerOutputDevices.length != 0 ? "connected" : "disconnected" text: isNetworkPrinter ? Cura.MachineManager.activeMachineNetworkGroupName : Cura.MachineManager.activeMachineName tooltip: Cura.MachineManager.activeMachineName - style: ButtonStyle { - background: Rectangle { - color: { + style: ButtonStyle + { + background: Rectangle + { + color: + { if (control.pressed) { return UM.Theme.getColor("sidebar_header_active"); } @@ -33,7 +38,8 @@ ToolButton { } Behavior on color { ColorAnimation { duration: 50; } } - UM.RecolorImage { + UM.RecolorImage + { id: downArrow anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -46,24 +52,27 @@ ToolButton { source: UM.Theme.getIcon("arrow_bottom") } - PrinterStatusIcon { + PrinterStatusIcon + { id: printerStatusIcon - visible: isNetworkPrinter + visible: printerConnected || isNetworkPrinter status: printerStatus - anchors { + anchors + { verticalCenter: parent.verticalCenter left: parent.left leftMargin: UM.Theme.getSize("sidebar_margin").width } } - Label { + Label + { id: sidebarComboBoxLabel color: UM.Theme.getColor("sidebar_header_text_active") text: control.text; elide: Text.ElideRight; - anchors.left: isNetworkPrinter ? printerStatusIcon.right : parent.left; - anchors.leftMargin: isNetworkPrinter ? UM.Theme.getSize("sidebar_lining").width : UM.Theme.getSize("sidebar_margin").width + anchors.left: printerStatusIcon.visible ? printerStatusIcon.right : parent.left; + anchors.leftMargin: printerStatusIcon.visible ? UM.Theme.getSize("sidebar_lining").width : UM.Theme.getSize("sidebar_margin").width anchors.right: downArrow.left; anchors.rightMargin: control.rightMargin; anchors.verticalCenter: parent.verticalCenter; @@ -74,14 +83,4 @@ ToolButton { } menu: PrinterMenu { } - - // Make the toolbutton react when the outputdevice changes - Connections - { - target: Cura.MachineManager - onOutputDevicesChanged: - { - base.isNetworkPrinter = Cura.MachineManager.activeMachineNetworkKey != "" - } - } } diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml index 52fd0e6556..999fecd7fd 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml @@ -66,6 +66,7 @@ Column configuration: modelData onActivateConfiguration: { + switchPopupState() Cura.MachineManager.applyRemoteConfiguration(configuration) } } diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml index eb0d5f5cff..d7ee2c68ee 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml @@ -13,54 +13,53 @@ Item id: configurationSelector property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null property var panelWidth: control.width - property var panelVisible: false - SyncButton { - onClicked: configurationSelector.state == "open" ? configurationSelector.state = "closed" : configurationSelector.state = "open" + function switchPopupState() + { + popup.visible ? popup.close() : popup.open() + } + + SyncButton + { + id: syncButton + onClicked: switchPopupState() outputDevice: connectedDevice } - Popup { + Popup + { + // TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property id: popup clip: true + closePolicy: Popup.CloseOnPressOutsideParent y: configurationSelector.height - UM.Theme.getSize("default_lining").height x: configurationSelector.width - width width: panelWidth - visible: panelVisible && connectedDevice != null + visible: false padding: UM.Theme.getSize("default_lining").width - contentItem: ConfigurationListView { + transformOrigin: Popup.Top + contentItem: ConfigurationListView + { id: configList width: panelWidth - 2 * popup.padding outputDevice: connectedDevice } - background: Rectangle { + background: Rectangle + { color: UM.Theme.getColor("setting_control") border.color: UM.Theme.getColor("setting_control_border") } - } - - states: [ - // This adds a second state to the container where the rectangle is farther to the right - State { - name: "open" - PropertyChanges { - target: popup - height: configList.computedHeight - } - }, - State { - name: "closed" - PropertyChanges { - target: popup - height: 0 - } - } - ] - transitions: [ - // This adds a transition that defaults to applying to all state changes - Transition { + exit: Transition + { // This applies a default NumberAnimation to any changes a state change makes to x or y properties - NumberAnimation { properties: "height"; duration: 200; easing.type: Easing.InOutQuad; } + NumberAnimation { property: "visible"; duration: 75; } } - ] + enter: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + NumberAnimation { property: "visible"; duration: 75; } + } + onClosed: visible = false + onOpened: visible = true + } } \ No newline at end of file diff --git a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml index f0394cc107..8fe9dacf9a 100644 --- a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml +++ b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml @@ -68,7 +68,8 @@ Button color: UM.Theme.getColor("text_emphasis") source: UM.Theme.getIcon("arrow_bottom") } - UM.RecolorImage { + UM.RecolorImage + { id: sidebarComboBoxLabel anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width @@ -86,22 +87,15 @@ Button label: Label {} } - onClicked: + Connections { - panelVisible = !panelVisible - } - - Connections { target: outputDevice - onUniqueConfigurationsChanged: { - updateOnSync() - } + onUniqueConfigurationsChanged: updateOnSync() } - Connections { + Connections + { target: Cura.MachineManager - onCurrentConfigurationChanged: { - updateOnSync() - } + onCurrentConfigurationChanged: updateOnSync() } } \ No newline at end of file diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 83302f9463..e35aef5f20 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -31,7 +31,7 @@ Menu MenuItem { text: "%1: %2 - %3".arg(model.name).arg(model.material).arg(model.variant) visible: base.shouldShowExtruders - enabled: UM.Selection.hasSelection + enabled: UM.Selection.hasSelection && model.enabled checkable: true checked: Cura.ExtruderManager.selectedObjectExtruders.indexOf(model.id) != -1 onTriggered: CuraActions.setExtruderForSelection(model.id) diff --git a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml index 19c36e6118..0753c83b17 100644 --- a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml +++ b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml @@ -1,8 +1,8 @@ // 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 2.7 +import QtQuick.Controls 1.4 import UM 1.2 as UM import Cura 1.0 as Cura @@ -12,44 +12,26 @@ Menu id: menu title: catalog.i18nc("@action:inmenu", "Visible Settings") + property QtObject settingVisibilityPresetsModel: CuraApplication.getSettingVisibilityPresetsModel() property bool showingSearchResults property bool showingAllSettings signal showAllSettings() signal showSettingVisibilityProfile() - MenuItem - { - text: catalog.i18nc("@action:inmenu", "Custom selection") - checkable: true - checked: !showingSearchResults && !showingAllSettings && Cura.SettingVisibilityPresetsModel.activePreset == "custom" - exclusiveGroup: group - onTriggered: - { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); - // Restore custom set from preference - UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings")); - showSettingVisibilityProfile(); - } - } - MenuSeparator { } - Instantiator { - model: Cura.SettingVisibilityPresetsModel + model: settingVisibilityPresetsModel MenuItem { text: model.name checkable: true - checked: model.id == Cura.SettingVisibilityPresetsModel.activePreset + checked: model.id == settingVisibilityPresetsModel.activePreset exclusiveGroup: group onTriggered: { - Cura.SettingVisibilityPresetsModel.setActivePreset(model.id); - - UM.Preferences.setValue("general/visible_settings", model.settings.join(";")); - + settingVisibilityPresetsModel.setActivePreset(model.id); showSettingVisibilityProfile(); } } diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index d2f653e650..3b1c10fbbd 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -36,8 +36,8 @@ TabView if (!base.containerId || !base.editingEnabled) { return "" } - var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.currentMaterialNode); - if (linkedMaterials.length <= 1) { + var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.currentMaterialNode, true); + if (linkedMaterials.length == 0) { return "" } return linkedMaterials.join(", "); @@ -99,6 +99,7 @@ TabView property var new_diameter_value: null; property var old_diameter_value: null; property var old_approximate_diameter_value: null; + property bool keyPressed: false onYes: { @@ -112,6 +113,16 @@ TabView properties.diameter = old_diameter_value; diameterSpinBox.value = properties.diameter; } + + onVisibilityChanged: + { + if (!visible && !keyPressed) + { + // If the user closes this dialog without clicking on any button, it's the same as clicking "No". + no(); + } + keyPressed = false; + } } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Display Name") } @@ -222,7 +233,7 @@ TabView var old_diameter = Cura.ContainerManager.getContainerProperty(base.containerId, "material_diameter", "value").toString(); var old_approximate_diameter = Cura.ContainerManager.getContainerMetaDataEntry(base.containerId, "approximate_diameter"); var new_approximate_diameter = getApproximateDiameter(value); - if (Cura.MachineManager.filterMaterialsByMachine && new_approximate_diameter != Cura.ExtruderManager.getActiveExtruderStack().approximateMaterialDiameter) + if (new_approximate_diameter != Cura.ExtruderManager.getActiveExtruderStack().approximateMaterialDiameter) { confirmDiameterChangeDialog.old_diameter_value = old_diameter; confirmDiameterChangeDialog.new_diameter_value = value; diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 7f06ffecde..042bd09828 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -364,6 +364,7 @@ Item } width: true ? (parent.width * 0.4) | 0 : parent.width + frameVisible: true ListView { diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml index ff35e27eeb..1726087e97 100644 --- a/resources/qml/Preferences/ProfilesPage.qml +++ b/resources/qml/Preferences/ProfilesPage.qml @@ -369,6 +369,7 @@ Item } width: true ? (parent.width * 0.4) | 0 : parent.width + frameVisible: true ListView { diff --git a/resources/qml/Preferences/ReadOnlySpinBox.qml b/resources/qml/Preferences/ReadOnlySpinBox.qml index 5d0666d306..1bbef82b1e 100644 --- a/resources/qml/Preferences/ReadOnlySpinBox.qml +++ b/resources/qml/Preferences/ReadOnlySpinBox.qml @@ -34,6 +34,8 @@ Item anchors.fill: parent onEditingFinished: base.editingFinished() + Keys.onEnterPressed: base.editingFinished() + Keys.onReturnPressed: base.editingFinished() } Label diff --git a/resources/qml/Preferences/ReadOnlyTextField.qml b/resources/qml/Preferences/ReadOnlyTextField.qml index 9407475a9b..38d07d7d6a 100644 --- a/resources/qml/Preferences/ReadOnlyTextField.qml +++ b/resources/qml/Preferences/ReadOnlyTextField.qml @@ -29,6 +29,8 @@ Item anchors.fill: parent onEditingFinished: base.editingFinished() + Keys.onEnterPressed: base.editingFinished() + Keys.onReturnPressed: base.editingFinished() } Label diff --git a/resources/qml/Preferences/SettingVisibilityPage.qml b/resources/qml/Preferences/SettingVisibilityPage.qml index f0c24e2cbe..b6b1c133ed 100644 --- a/resources/qml/Preferences/SettingVisibilityPage.qml +++ b/resources/qml/Preferences/SettingVisibilityPage.qml @@ -13,6 +13,8 @@ UM.PreferencesPage { title: catalog.i18nc("@title:tab", "Setting Visibility"); + property QtObject settingVisibilityPresetsModel: CuraApplication.getSettingVisibilityPresetsModel() + property int scrollToIndex: 0 signal scrollToSection( string key ) @@ -27,8 +29,7 @@ UM.PreferencesPage // After calling this function update Setting visibility preset combobox. // Reset should set default setting preset ("Basic") - visibilityPreset.setDefaultPreset() - + visibilityPreset.currentIndex = 1 } resetEnabled: true; @@ -37,8 +38,6 @@ UM.PreferencesPage id: base; anchors.fill: parent; - property bool inhibitSwitchToCustom: false - CheckBox { id: toggleVisibleSettings @@ -112,11 +111,6 @@ UM.PreferencesPage ComboBox { - function setDefaultPreset() - { - visibilityPreset.currentIndex = 0 - } - id: visibilityPreset width: 150 * screenScaleFactor anchors @@ -125,51 +119,25 @@ UM.PreferencesPage right: parent.right } - model: ListModel - { - id: visibilityPresetsModel - Component.onCompleted: - { - visibilityPresetsModel.append({text: catalog.i18nc("@action:inmenu", "Custom selection"), id: "custom"}); - - var presets = Cura.SettingVisibilityPresetsModel; - for(var i = 0; i < presets.rowCount(); i++) - { - visibilityPresetsModel.append({text: presets.getItem(i)["name"], id: presets.getItem(i)["id"]}); - } - } - } + model: settingVisibilityPresetsModel + textRole: "name" currentIndex: { // Load previously selected preset. - var index = Cura.SettingVisibilityPresetsModel.find("id", Cura.SettingVisibilityPresetsModel.activePreset); - if(index == -1) + var index = settingVisibilityPresetsModel.find("id", settingVisibilityPresetsModel.activePreset) + if (index == -1) { - return 0; + return 0 } - return index + 1; // "Custom selection" entry is added in front, so index is off by 1 + return index } onActivated: { - base.inhibitSwitchToCustom = true; - var preset_id = visibilityPresetsModel.get(index).id; - Cura.SettingVisibilityPresetsModel.setActivePreset(preset_id); - - UM.Preferences.setValue("cura/active_setting_visibility_preset", preset_id); - if (preset_id != "custom") - { - UM.Preferences.setValue("general/visible_settings", Cura.SettingVisibilityPresetsModel.getItem(index - 1).settings.join(";")); - // "Custom selection" entry is added in front, so index is off by 1 - } - else - { - // Restore custom set from preference - UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings")); - } - base.inhibitSwitchToCustom = false; + var preset_id = settingVisibilityPresetsModel.getItem(index).id; + settingVisibilityPresetsModel.setActivePreset(preset_id); } } @@ -199,16 +167,7 @@ UM.PreferencesPage exclude: ["machine_settings", "command_line_settings"] showAncestors: true expanded: ["*"] - visibilityHandler: UM.SettingPreferenceVisibilityHandler - { - onVisibilityChanged: - { - if(Cura.SettingVisibilityPresetsModel.activePreset != "" && !base.inhibitSwitchToCustom) - { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); - } - } - } + visibilityHandler: UM.SettingPreferenceVisibilityHandler {} } delegate: Loader diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index a7141262a9..56c86f1034 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -12,9 +12,20 @@ Item property alias color: background.color property var extruderModel property var position: index - //width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) implicitWidth: parent.width implicitHeight: UM.Theme.getSize("sidebar_extruder_box").height + + UM.SettingPropertyProvider + { + id: extruderTemperature + containerStackId: Cura.ExtruderManager.extruderIds[position] + key: "material_print_temperature" + watchedProperties: ["value", "minimum_value", "maximum_value", "resolve"] + storeIndex: 0 + + property var resolve: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId ? properties.resolve : "None" + } + Rectangle { id: background @@ -34,12 +45,11 @@ Item { id: extruderTargetTemperature text: Math.round(extruderModel.targetHotendTemperature) + "°C" - //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.targetHotendTemperatures[index] != null) ? Math.round(connectedPrinter.targetHotendTemperatures[index]) + "°C" : "" font: UM.Theme.getFont("small") color: UM.Theme.getColor("text_inactive") anchors.right: parent.right anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.bottom: extruderTemperature.bottom + anchors.bottom: extruderCurrentTemperature.bottom MouseArea //For tooltip. { @@ -52,7 +62,7 @@ Item { base.showTooltip( base, - {x: 0, y: extruderTargetTemperature.mapToItem(base, 0, -parent.height / 4).y}, + {x: 0, y: extruderTargetTemperature.mapToItem(base, 0, Math.floor(-parent.height / 4)).y}, catalog.i18nc("@tooltip", "The target temperature of the hotend. The hotend will heat up or cool down towards this temperature. If this is 0, the hotend heating is turned off.") ); } @@ -65,9 +75,8 @@ Item } Label //Temperature indication. { - id: extruderTemperature + id: extruderCurrentTemperature text: Math.round(extruderModel.hotendTemperature) + "°C" - //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.hotendTemperatures[index] != null) ? Math.round(connectedPrinter.hotendTemperatures[index]) + "°C" : "" color: UM.Theme.getColor("text") font: UM.Theme.getFont("large") anchors.right: extruderTargetTemperature.left @@ -76,7 +85,7 @@ Item MouseArea //For tooltip. { - id: extruderTemperatureTooltipArea + id: extruderCurrentTemperatureTooltipArea hoverEnabled: true anchors.fill: parent onHoveredChanged: @@ -85,8 +94,8 @@ Item { base.showTooltip( base, - {x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y}, - catalog.i18nc("@tooltip", "The current temperature of this extruder.") + {x: 0, y: parent.mapToItem(base, 0, Math.floor(-parent.height / 4)).y}, + catalog.i18nc("@tooltip", "The current temperature of this hotend.") ); } else @@ -97,6 +106,272 @@ Item } } + Rectangle //Input field for pre-heat temperature. + { + id: preheatTemperatureControl + color: !enabled ? UM.Theme.getColor("setting_control_disabled") : showError ? UM.Theme.getColor("setting_validation_error_background") : UM.Theme.getColor("setting_validation_ok") + property var showError: + { + if(extruderTemperature.properties.maximum_value != "None" && extruderTemperature.properties.maximum_value < Math.floor(preheatTemperatureInput.text)) + { + return true; + } else + { + return false; + } + } + enabled: + { + if (extruderModel == null) + { + return false; //Can't preheat if not connected. + } + if (!connectedPrinter.acceptsCommands) + { + return false; //Not allowed to do anything. + } + if (connectedPrinter.activePrinter && connectedPrinter.activePrinter.activePrintJob) + { + if((["printing", "pre_print", "resuming", "pausing", "paused", "error", "offline"]).indexOf(connectedPrinter.activePrinter.activePrintJob.state) != -1) + { + return false; //Printer is in a state where it can't react to pre-heating. + } + } + return true; + } + border.width: UM.Theme.getSize("default_lining").width + border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") + anchors.right: preheatButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.bottom: parent.bottom + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + width: UM.Theme.getSize("monitor_preheat_temperature_control").width + height: UM.Theme.getSize("monitor_preheat_temperature_control").height + visible: extruderModel != null ? enabled && extruderModel.canPreHeatHotends && !extruderModel.isPreheating : true + Rectangle //Highlight of input field. + { + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_lining").width + color: UM.Theme.getColor("setting_control_highlight") + opacity: preheatTemperatureControl.hovered ? 1.0 : 0 + } + MouseArea //Change cursor on hovering. + { + id: preheatTemperatureInputMouseArea + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.IBeamCursor + + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: preheatTemperatureInputMouseArea.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the hotend to.") + ); + } + else + { + base.hideTooltip(); + } + } + } + Label + { + id: unit + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.verticalCenter: parent.verticalCenter + + text: "°C"; + color: UM.Theme.getColor("setting_unit") + font: UM.Theme.getFont("default") + } + TextInput + { + id: preheatTemperatureInput + font: UM.Theme.getFont("default") + color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") + selectByMouse: true + maximumLength: 5 + enabled: parent.enabled + validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.right: unit.left + anchors.verticalCenter: parent.verticalCenter + renderType: Text.NativeRendering + + Component.onCompleted: + { + if (!extruderTemperature.properties.value) + { + text = ""; + } + else + { + text = extruderTemperature.properties.value; + } + } + } + } + + Button //The pre-heat button. + { + id: preheatButton + height: UM.Theme.getSize("setting_control").height + visible: extruderModel != null ? extruderModel.canPreHeatHotends: true + enabled: + { + if (!preheatTemperatureControl.enabled) + { + return false; //Not connected, not authenticated or printer is busy. + } + if (extruderModel.isPreheating) + { + return true; + } + if (extruderTemperature.properties.minimum_value != "None" && Math.floor(preheatTemperatureInput.text) < Math.floor(extruderTemperature.properties.minimum_value)) + { + return false; //Target temperature too low. + } + if (extruderTemperature.properties.maximum_value != "None" && Math.floor(preheatTemperatureInput.text) > Math.floor(extruderTemperature.properties.maximum_value)) + { + return false; //Target temperature too high. + } + if (Math.floor(preheatTemperatureInput.text) == 0) + { + return false; //Setting the temperature to 0 is not allowed (since that cancels the pre-heating). + } + return true; //Preconditions are met. + } + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").width + style: ButtonStyle { + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("default_margin").width * 2) + border.color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_border"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_border"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_border"); + } + else + { + return UM.Theme.getColor("action_button_border"); + } + } + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered"); + } + else + { + return UM.Theme.getColor("action_button"); + } + } + Behavior on color + { + ColorAnimation + { + duration: 50 + } + } + + Label + { + id: actualLabel + anchors.centerIn: parent + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_text"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_text"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_text"); + } + else + { + return UM.Theme.getColor("action_button_text"); + } + } + font: UM.Theme.getFont("action_button") + text: + { + if(extruderModel == null) + { + return "" + } + if(extruderModel.isPreheating ) + { + return catalog.i18nc("@button Cancel pre-heating", "Cancel") + } else + { + return catalog.i18nc("@button", "Pre-heat") + } + } + } + } + } + + onClicked: + { + if (!extruderModel.isPreheating) + { + extruderModel.preheatHotend(preheatTemperatureInput.text, 900); + } + else + { + extruderModel.cancelPreheatHotend(); + } + } + + onHoveredChanged: + { + if (hovered) + { + base.showTooltip( + base, + {x: 0, y: preheatButton.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip of pre-heat", "Heat the hotend in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the hotend to heat up when you're ready to print.") + ); + } + else + { + base.hideTooltip(); + } + } + } + Rectangle //Material colour indication. { id: materialColor diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index bc89da2251..385977141c 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -114,21 +114,24 @@ Item { return false; //Not allowed to do anything. } - if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") + if (connectedPrinter.activePrinter && connectedPrinter.activePrinter.activePrintJob) { - return false; //Printer is in a state where it can't react to pre-heating. + if((["printing", "pre_print", "resuming", "pausing", "paused", "error", "offline"]).indexOf(connectedPrinter.activePrinter.activePrintJob.state) != -1) + { + return false; //Printer is in a state where it can't react to pre-heating. + } } return true; } border.width: UM.Theme.getSize("default_lining").width border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: preheatButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.bottom: parent.bottom anchors.bottomMargin: UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("setting_control").width - height: UM.Theme.getSize("setting_control").height - visible: printerModel != null ? printerModel.canPreHeatBed: true + width: UM.Theme.getSize("monitor_preheat_temperature_control").width + height: UM.Theme.getSize("monitor_preheat_temperature_control").height + visible: printerModel != null ? enabled && printerModel.canPreHeatBed && !printerModel.isPreheating : true Rectangle //Highlight of input field. { anchors.fill: parent @@ -159,18 +162,29 @@ Item } } } + Label + { + id: unit + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.verticalCenter: parent.verticalCenter + + text: "°C"; + color: UM.Theme.getColor("setting_unit") + font: UM.Theme.getFont("default") + } TextInput { id: preheatTemperatureInput font: UM.Theme.getFont("default") color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") selectByMouse: true - maximumLength: 10 + maximumLength: 5 enabled: parent.enabled validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width - anchors.right: parent.right + anchors.right: unit.left anchors.verticalCenter: parent.verticalCenter renderType: Text.NativeRendering diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml index 2ddbb135c7..38b1c2cab0 100644 --- a/resources/qml/Settings/SettingExtruder.qml +++ b/resources/qml/Settings/SettingExtruder.qml @@ -215,7 +215,8 @@ SettingItem { text: model.name renderType: Text.NativeRendering - color: { + color: + { if (model.enabled) { UM.Theme.getColor("setting_control_text") } else { diff --git a/resources/qml/Settings/SettingOptionalExtruder.qml b/resources/qml/Settings/SettingOptionalExtruder.qml index f49b7035d7..a370ec6259 100644 --- a/resources/qml/Settings/SettingOptionalExtruder.qml +++ b/resources/qml/Settings/SettingOptionalExtruder.qml @@ -27,8 +27,19 @@ SettingItem onActivated: { - forceActiveFocus(); - propertyProvider.setPropertyValue("value", model.getItem(index).index); + if (model.getItem(index).enabled) + { + forceActiveFocus(); + propertyProvider.setPropertyValue("value", model.getItem(index).index); + } else + { + if (propertyProvider.properties.value == -1) + { + control.currentIndex = model.rowCount() - 1; // we know the last item is "Not overriden" + } else { + control.currentIndex = propertyProvider.properties.value; // revert to the old value + } + } } onActiveFocusChanged: @@ -192,7 +203,14 @@ SettingItem { text: model.name renderType: Text.NativeRendering - color: UM.Theme.getColor("setting_control_text") + color: + { + if (model.enabled) { + UM.Theme.getColor("setting_control_text") + } else { + UM.Theme.getColor("action_button_disabled_text"); + } + } font: UM.Theme.getFont("default") elide: Text.ElideRight verticalAlignment: Text.AlignVCenter diff --git a/resources/qml/Settings/SettingTextField.qml b/resources/qml/Settings/SettingTextField.qml index 4693bec50d..c2c04ce36c 100644 --- a/resources/qml/Settings/SettingTextField.qml +++ b/resources/qml/Settings/SettingTextField.qml @@ -13,11 +13,17 @@ SettingItem property string textBeforeEdit property bool textHasChanged + property bool focusGainedByClick: false onFocusReceived: { textHasChanged = false; textBeforeEdit = focusItem.text; - focusItem.selectAll(); + + if(!focusGainedByClick) + { + // select all text when tabbing through fields (but not when selecting a field with the mouse) + focusItem.selectAll(); + } } contents: Rectangle @@ -93,14 +99,6 @@ SettingItem font: UM.Theme.getFont("default") } - MouseArea - { - id: mouseArea - anchors.fill: parent; - //hoverEnabled: true; - cursorShape: Qt.IBeamCursor - } - TextInput { id: input @@ -142,6 +140,7 @@ SettingItem { base.focusReceived(); } + base.focusGainedByClick = false; } color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") @@ -178,6 +177,22 @@ SettingItem } when: !input.activeFocus } + + MouseArea + { + id: mouseArea + anchors.fill: parent; + + cursorShape: Qt.IBeamCursor + + onPressed: { + if(!input.activeFocus) { + base.focusGainedByClick = true; + input.forceActiveFocus(); + } + mouse.accepted = false; + } + } } } } diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 235dfac91a..cf9697210b 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -15,6 +15,7 @@ Item { id: base; + property QtObject settingVisibilityPresetsModel: CuraApplication.getSettingVisibilityPresetsModel() property Action configureSettings property bool findingSettings property bool showingAllSettings @@ -439,6 +440,7 @@ Item key: model.key ? model.key : "" watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ] storeIndex: 0 + removeUnusedValue: model.resolve == undefined } Connections @@ -562,9 +564,9 @@ Item { definitionsModel.hide(contextMenu.key); // visible settings have changed, so we're no longer showing a preset - if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) + if (settingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + settingVisibilityPresetsModel.setActivePreset("custom"); } } } @@ -594,16 +596,16 @@ Item definitionsModel.show(contextMenu.key); } // visible settings have changed, so we're no longer showing a preset - if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) + if (settingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + settingVisibilityPresetsModel.setActivePreset("custom"); } } } MenuItem { //: Settings context menu action - text: catalog.i18nc("@action:menu", "Configure setting visiblity..."); + text: catalog.i18nc("@action:menu", "Configure setting visibility..."); onTriggered: Cura.Actions.configureSettingVisibility.trigger(contextMenu); } diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index 5211ee5a1d..1c1bfca7c3 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -87,7 +87,8 @@ Rectangle } } - MachineSelection { + MachineSelection + { id: machineSelection width: base.width - configSelection.width - separator.width height: UM.Theme.getSize("sidebar_header").height @@ -105,9 +106,10 @@ Rectangle anchors.left: machineSelection.right } - ConfigurationSelection { + ConfigurationSelection + { id: configSelection - visible: isNetworkPrinter && !sidebar.monitoringPrint && !sidebar.hideSettings + visible: isNetworkPrinter && printerConnected width: visible ? Math.round(base.width * 0.15) : 0 height: UM.Theme.getSize("sidebar_header").height anchors.top: base.top diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 5cd0446b36..1ba04c387e 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -178,6 +178,7 @@ Column text: catalog.i18nc("@action:inmenu", "Disable Extruder") onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) visible: extruder_enabled + enabled: Cura.MachineManager.numberExtrudersEnabled > 1 } } @@ -185,22 +186,34 @@ Column { background: Item { - Rectangle + function buttonBackgroundColor(index) { - anchors.fill: parent - border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width - border.color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_border") : - control.hovered ? UM.Theme.getColor("action_button_hovered_border") : - UM.Theme.getColor("action_button_border") - color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active") : - control.hovered ? UM.Theme.getColor("action_button_hovered") : - UM.Theme.getColor("action_button") - Behavior on color { ColorAnimation { duration: 50; } } + var extruder = Cura.MachineManager.getExtruder(index) + if (extruder.isEnabled) { + return (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active") : + control.hovered ? UM.Theme.getColor("action_button_hovered") : + UM.Theme.getColor("action_button") + } else { + return UM.Theme.getColor("action_button_disabled") + } + } + + function buttonBorderColor(index) + { + var extruder = Cura.MachineManager.getExtruder(index) + if (extruder.isEnabled) { + return (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_border") : + control.hovered ? UM.Theme.getColor("action_button_hovered_border") : + UM.Theme.getColor("action_button_border") + } else { + return UM.Theme.getColor("action_button_disabled_border") + } } function buttonColor(index) { var extruder = Cura.MachineManager.getExtruder(index); - if (extruder.isEnabled) { + if (extruder.isEnabled) + { return ( control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_text") : control.hovered ? UM.Theme.getColor("action_button_hovered_text") : @@ -210,10 +223,20 @@ Column } } + Rectangle + { + anchors.fill: parent + border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width + border.color: buttonBorderColor(index) + color: buttonBackgroundColor(index) + Behavior on color { ColorAnimation { duration: 50; } } + } + Item { id: extruderButtonFace anchors.centerIn: parent + width: { var extruderTextWidth = extruderStaticText.visible ? extruderStaticText.width : 0; var iconWidth = extruderIconItem.width; @@ -501,6 +524,8 @@ Column source: UM.Theme.getIcon("warning") width: UM.Theme.getSize("section_icon").width height: UM.Theme.getSize("section_icon").height + sourceSize.width: width + sourceSize.height: height color: UM.Theme.getColor("material_compatibility_warning") visible: !Cura.MachineManager.isCurrentSetupSupported } @@ -522,9 +547,7 @@ Column hoverEnabled: true onClicked: { // open the material URL with web browser - var version = UM.Application.version; - var machineName = Cura.MachineManager.activeMachine.definition.id; - var url = "https://ultimaker.com/materialcompatibility/" + version + "/" + machineName + "?utm_source=cura&utm_medium=software&utm_campaign=resources"; + var url = "https://ultimaker.com/incoming-links/cura/material-compatibilty" Qt.openUrlExternally(url); } onEntered: { diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index 41ecb529eb..1feabfb40f 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -111,7 +111,6 @@ Item // Set selected value if (Cura.MachineManager.activeQualityType == qualityItem.quality_type) { - // set to -1 when switching to user created profile so all ticks are clickable if (Cura.SimpleModeSettingsManager.isProfileUserCreated) { qualityModel.qualitySliderActiveIndex = -1 @@ -474,18 +473,7 @@ Item onClicked: { // if the current profile is user-created, switch to a built-in quality - if (Cura.SimpleModeSettingsManager.isProfileUserCreated) - { - if (Cura.QualityProfilesDropDownMenuModel.rowCount() > 0) - { - var item = Cura.QualityProfilesDropDownMenuModel.getItem(0); - Cura.MachineManager.activeQualityGroup = item.quality_group; - } - } - if (Cura.SimpleModeSettingsManager.isProfileCustomized) - { - discardOrKeepProfileChangesDialog.show() - } + Cura.MachineManager.resetToUseDefaultQuality() } onEntered: { @@ -594,7 +582,9 @@ Item // Update value only if the Recomended mode is Active, // Otherwise if I change the value in the Custom mode the Recomended view will try to repeat // same operation - if (UM.Preferences.getValue("cura/active_mode") == 0) { + var active_mode = UM.Preferences.getValue("cura/active_mode") + + if (active_mode == 0 || active_mode == "simple") { Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) } } diff --git a/resources/shaders/camera_distance.shader b/resources/shaders/camera_distance.shader new file mode 100644 index 0000000000..e6e894a2f6 --- /dev/null +++ b/resources/shaders/camera_distance.shader @@ -0,0 +1,83 @@ +[shaders] +vertex = + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewProjectionMatrix; + + attribute highp vec4 a_vertex; + + varying highp vec3 v_vertex; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_viewProjectionMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + } + +fragment = + uniform highp vec3 u_viewPosition; + + varying highp vec3 v_vertex; + + void main() + { + highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron + + vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter + encoded.b = floor(distance_to_camera / 65536.0); + encoded.g = floor((distance_to_camera - encoded.b * 65536.0) / 256.0); + encoded.r = floor(distance_to_camera - encoded.b * 65536.0 - encoded.g * 256.0); + + gl_FragColor.rgb = encoded / 255.; + gl_FragColor.a = 1.0; + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewProjectionMatrix; + + in highp vec4 a_vertex; + + out highp vec3 v_vertex; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_viewProjectionMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + } + +fragment41core = + #version 410 + uniform highp vec3 u_viewPosition; + + in highp vec3 v_vertex; + + out vec4 frag_color; + + void main() + { + highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron + + vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter + encoded.r = floor(distance_to_camera / 65536.0); + encoded.g = floor((distance_to_camera - encoded.r * 65536.0) / 256.0); + encoded.b = floor(distance_to_camera - encoded.r * 65536.0 - encoded.g * 256.0); + + frag_color.rgb = encoded / 255.; + frag_color.a = 1.0; + } + +[defaults] + +[bindings] +u_modelMatrix = model_matrix +u_viewProjectionMatrix = view_projection_matrix +u_normalMatrix = normal_matrix +u_viewPosition = view_position + +[attributes] +a_vertex = vertex diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 80a5eec09c..5fbe36fcdb 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -84,16 +84,16 @@ "tab_background": [39, 44, 48, 255], "action_button": [39, 44, 48, 255], - "action_button_text": [255, 255, 255, 101], + "action_button_text": [255, 255, 255, 200], "action_button_border": [255, 255, 255, 30], "action_button_hovered": [39, 44, 48, 255], "action_button_hovered_text": [255, 255, 255, 255], "action_button_hovered_border": [255, 255, 255, 30], "action_button_active": [39, 44, 48, 30], "action_button_active_text": [255, 255, 255, 255], - "action_button_active_border": [255, 255, 255, 30], + "action_button_active_border": [255, 255, 255, 100], "action_button_disabled": [39, 44, 48, 255], - "action_button_disabled_text": [255, 255, 255, 101], + "action_button_disabled_text": [255, 255, 255, 80], "action_button_disabled_border": [255, 255, 255, 30], "scrollbar_background": [39, 44, 48, 0], diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index c0b71ac618..4f4b2306a8 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], + "monitor_preheat_temperature_control": [4.5, 2.0], + "modal_window_minimum": [60.0, 45], "license_window_minimum": [45, 45], "wizard_progress": [10.0, 0.0],