diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 275fce6995..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: @@ -239,7 +239,7 @@ class BuildVolume(SceneNode): # Group nodes should override the _outside_buildarea property of their children. for group_node in group_nodes: for child_node in group_node.getAllChildren(): - child_node.setOutsideBuildArea(group_node.isOutsideBuildArea) + child_node.setOutsideBuildArea(group_node.isOutsideBuildArea()) ## Update the outsideBuildArea of a single node, given bounds or current build volume def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = 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 f39c2ba554..b7731c5c8c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -4,7 +4,7 @@ #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 @@ -60,18 +60,20 @@ from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.NozzleModel import NozzleModel from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel - from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel - from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel +from cura.Machines.Models.QualityManagementModel import QualityManagementModel +from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel +from cura.Machines.Models.MachineManagementModel import MachineManagementModel + +from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Machines.VariantManager import VariantManager -from cura.Machines.Models.QualityManagementModel import QualityManagementModel from . import PlatformPhysics from . import BuildVolume @@ -88,8 +90,8 @@ from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.UserChangesModel import UserChangesModel from cura.Settings.ExtrudersModel import ExtrudersModel from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler -from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Settings.ContainerManager import ContainerManager +from cura.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.ObjectsModel import ObjectsModel @@ -139,15 +141,10 @@ class CuraApplication(QtApplication): MachineStack = Resources.UserType + 7 ExtruderStack = Resources.UserType + 8 DefinitionChangesContainer = Resources.UserType + 9 + SettingVisibilityPreset = Resources.UserType + 10 Q_ENUMS(ResourceTypes) - # FIXME: This signal belongs to the MachineManager, but the CuraEngineBackend plugin requires on it. - # Because plugins are initialized before the ContainerRegistry, putting this signal in MachineManager - # will make it initialized before ContainerRegistry does, and it won't find the active machine, thus - # Cura will always show the Add Machine Dialog upon start. - stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished - def __init__(self, **kwargs): # this list of dir names will be used by UM to detect an old cura directory for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]: @@ -192,6 +189,7 @@ class CuraApplication(QtApplication): Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders") Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances") Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes") + Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility") ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality") ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes") @@ -224,12 +222,14 @@ class CuraApplication(QtApplication): self._machine_manager = None # This is initialized on demand. self._extruder_manager = None self._material_manager = None + self._quality_manager = None self._object_manager = None self._build_plate_model = None self._multi_build_plate_model = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None self._cura_scene_controller = None + self._machine_error_checker = None self._additional_components = {} # Components to add to certain areas in the interface @@ -286,10 +286,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,19 +381,9 @@ class CuraApplication(QtApplication): preferences.setDefault("local_file/last_used_type", "text/x-gcode") - setting_visibily_preset_names = self.getVisibilitySettingPresetTypes() - preferences.setDefault("general/visible_settings_preset", setting_visibily_preset_names) + default_visibility_profile = SettingVisibilityPresetsModel.getInstance().getItem(0) - preset_setting_visibility_choice = Preferences.getInstance().getValue("general/preset_setting_visibility_choice") - - default_preset_visibility_group_name = "Basic" - if preset_setting_visibility_choice == "" or preset_setting_visibility_choice is None: - if preset_setting_visibility_choice not in setting_visibily_preset_names: - preset_setting_visibility_choice = default_preset_visibility_group_name - - visible_settings = self.getVisibilitySettingPreset(settings_preset_name = preset_setting_visibility_choice) - preferences.setDefault("general/visible_settings", visible_settings) - preferences.setDefault("general/preset_setting_visibility_choice", preset_setting_visibility_choice) + preferences.setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) self.applicationShuttingDown.connect(self.saveSettings) self.engineCreatedSignal.connect(self._onEngineCreated) @@ -405,91 +400,6 @@ class CuraApplication(QtApplication): CuraApplication.Created = True - @pyqtSlot(str, result = str) - def getVisibilitySettingPreset(self, settings_preset_name) -> str: - result = self._loadPresetSettingVisibilityGroup(settings_preset_name) - formatted_preset_settings = self._serializePresetSettingVisibilityData(result) - - return formatted_preset_settings - - ## Serialise the given preset setting visibitlity group dictionary into a string which is concatenated by ";" - # - def _serializePresetSettingVisibilityData(self, settings_data: dict) -> str: - result_string = "" - - for key in settings_data: - result_string += key + ";" - for value in settings_data[key]: - result_string += value + ";" - - return result_string - - ## Load the preset setting visibility group with the given name - # - def _loadPresetSettingVisibilityGroup(self, visibility_preset_name) -> Dict[str, str]: - preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups) - - result = {} - right_preset_found = False - - for item in os.listdir(preset_dir): - file_path = os.path.join(preset_dir, item) - if not os.path.isfile(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"): - continue - - if parser["general"]["name"] == visibility_preset_name: - right_preset_found = True - for section in parser.sections(): - if section == 'general': - continue - else: - section_settings = [] - for option in parser[section].keys(): - section_settings.append(option) - - result[section] = section_settings - - if right_preset_found: - break - - except Exception as e: - Logger.log("e", "Failed to load setting visibility preset %s: %s", file_path, str(e)) - - return result - - ## Check visibility setting preset folder and returns available types - # - def getVisibilitySettingPresetTypes(self): - preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups) - result = {} - - for item in os.listdir(preset_dir): - file_path = os.path.join(preset_dir, item) - if not os.path.isfile(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") and not parser.has_option("general", "weight"): - continue - - result[parser["general"]["weight"]] = parser["general"]["name"] - - except Exception as e: - Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e)) - - return result def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -743,19 +653,28 @@ class CuraApplication(QtApplication): self.preRun() container_registry = ContainerRegistry.getInstance() + + Logger.log("i", "Initializing variant manager") self._variant_manager = VariantManager(container_registry) self._variant_manager.initialize() + Logger.log("i", "Initializing material manager") from cura.Machines.MaterialManager import MaterialManager self._material_manager = MaterialManager(container_registry, parent = self) self._material_manager.initialize() + Logger.log("i", "Initializing quality manager") from cura.Machines.QualityManager import QualityManager self._quality_manager = QualityManager(container_registry, parent = self) self._quality_manager.initialize() + Logger.log("i", "Initializing machine manager") self._machine_manager = MachineManager(self) + Logger.log("i", "Initializing machine error checker") + self._machine_error_checker = MachineErrorChecker(self) + self._machine_error_checker.initialize() + # Check if we should run as single instance or not self._setUpSingleInstanceServer() @@ -781,8 +700,11 @@ class CuraApplication(QtApplication): self._openFile(file_name) self.started = True + self.initializationFinished.emit() self.exec_() + initializationFinished = pyqtSignal() + ## Run Cura without GUI elements and interaction (server mode). def runWithoutGUI(self): self._use_gui = False @@ -847,6 +769,9 @@ class CuraApplication(QtApplication): def hasGui(self): return self._use_gui + def getMachineErrorChecker(self, *args) -> MachineErrorChecker: + return self._machine_error_checker + def getMachineManager(self, *args) -> MachineManager: if self._machine_manager is None: self._machine_manager = MachineManager(self) @@ -961,6 +886,7 @@ class CuraApplication(QtApplication): qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel") qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel") qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel") + qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel") qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0, "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel) @@ -973,6 +899,7 @@ class CuraApplication(QtApplication): 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"))) @@ -1048,6 +975,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/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py new file mode 100644 index 0000000000..37de4f30ce --- /dev/null +++ b/cura/Machines/MachineErrorChecker.py @@ -0,0 +1,181 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import time + +from collections import deque + +from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty + +from UM.Application import Application +from UM.Logger import Logger +from UM.Settings.SettingDefinition import SettingDefinition +from UM.Settings.Validator import ValidatorState + + +# +# This class performs setting error checks for the currently active machine. +# +# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. +# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key +# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should +# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait +# for it to finish the complete work. +# +class MachineErrorChecker(QObject): + + def __init__(self, parent = None): + super().__init__(parent) + + self._global_stack = None + + self._has_errors = True # Result of the error check, indicating whether there are errors in the stack + self._error_keys = set() # A set of settings keys that have errors + self._error_keys_in_progress = set() # The variable that stores the results of the currently in progress check + + self._stacks_and_keys_to_check = None # a FIFO queue of tuples (stack, key) to check for errors + + self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new + # error check needs to take place while there is already one running at the moment. + self._check_in_progress = False # Whether there is an error check running in progress at the moment. + + self._application = Application.getInstance() + self._machine_manager = self._application.getMachineManager() + + self._start_time = 0 # measure checking time + + # This timer delays the starting of error check so we can react less frequently if the user is frequently + # changing settings. + self._error_check_timer = QTimer(self) + self._error_check_timer.setInterval(100) + self._error_check_timer.setSingleShot(True) + + def initialize(self): + self._error_check_timer.timeout.connect(self._rescheduleCheck) + + # Reconnect all signals when the active machine gets changed. + self._machine_manager.globalContainerChanged.connect(self._onMachineChanged) + + # Whenever the machine settings get changed, we schedule an error check. + self._machine_manager.globalContainerChanged.connect(self.startErrorCheck) + self._machine_manager.globalValueChanged.connect(self.startErrorCheck) + + self._onMachineChanged() + + def _onMachineChanged(self): + if self._global_stack: + self._global_stack.propertyChanged.disconnect(self.startErrorCheck) + self._global_stack.containersChanged.disconnect(self.startErrorCheck) + + for extruder in self._global_stack.extruders.values(): + extruder.propertyChanged.disconnect(self.startErrorCheck) + extruder.containersChanged.disconnect(self.startErrorCheck) + + self._global_stack = self._machine_manager.activeMachine + + if self._global_stack: + self._global_stack.propertyChanged.connect(self.startErrorCheck) + self._global_stack.containersChanged.connect(self.startErrorCheck) + + for extruder in self._global_stack.extruders.values(): + extruder.propertyChanged.connect(self.startErrorCheck) + extruder.containersChanged.connect(self.startErrorCheck) + + hasErrorUpdated = pyqtSignal() + needToWaitForResultChanged = pyqtSignal() + errorCheckFinished = pyqtSignal() + + @pyqtProperty(bool, notify = hasErrorUpdated) + def hasError(self) -> bool: + return self._has_errors + + @pyqtProperty(bool, notify = needToWaitForResultChanged) + def needToWaitForResult(self) -> bool: + return self._need_to_check or self._check_in_progress + + # Starts the error check timer to schedule a new error check. + def startErrorCheck(self, *args): + if not self._check_in_progress: + self._need_to_check = True + self.needToWaitForResultChanged.emit() + self._error_check_timer.start() + + # This function is called by the timer to reschedule a new error check. + # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag + # to notify the current check to stop and start a new one. + def _rescheduleCheck(self): + if self._check_in_progress and not self._need_to_check: + self._need_to_check = True + self.needToWaitForResultChanged.emit() + return + + self._error_keys_in_progress = set() + self._need_to_check = False + self.needToWaitForResultChanged.emit() + + global_stack = self._machine_manager.activeMachine + if global_stack is None: + Logger.log("i", "No active machine, nothing to check.") + return + + # Populate the (stack, key) tuples to check + self._stacks_and_keys_to_check = deque() + for stack in [global_stack] + list(global_stack.extruders.values()): + for key in stack.getAllKeys(): + self._stacks_and_keys_to_check.append((stack, key)) + + self._application.callLater(self._checkStack) + self._start_time = time.time() + Logger.log("d", "New error check scheduled.") + + def _checkStack(self): + if self._need_to_check: + Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.") + self._check_in_progress = False + self._application.callLater(self.startErrorCheck) + return + + self._check_in_progress = True + + # If there is nothing to check any more, it means there is no error. + if not self._stacks_and_keys_to_check: + # Finish + self._setResult(False) + return + + # Get the next stack and key to check + stack, key = self._stacks_and_keys_to_check.popleft() + + enabled = stack.getProperty(key, "enabled") + if not enabled: + self._application.callLater(self._checkStack) + return + + validation_state = stack.getProperty(key, "validationState") + if validation_state is None: + # Setting is not validated. This can happen if there is only a setting definition. + # We do need to validate it, because a setting definitions value can be set by a function, which could + # be an invalid setting. + definition = stack.getSettingDefinition(key) + validator_type = SettingDefinition.getValidatorForType(definition.type) + if validator_type: + validator = validator_type(key) + validation_state = validator(stack) + if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): + # Finish + self._setResult(True) + return + + # Schedule the check for the next key + self._application.callLater(self._checkStack) + + def _setResult(self, result: bool): + if result != self._has_errors: + self._has_errors = result + self.hasErrorUpdated.emit() + self._machine_manager.stacksValidationChanged.emit() + self._need_to_check = False + self._check_in_progress = False + self.needToWaitForResultChanged.emit() + self.errorCheckFinished.emit() + Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time) diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index 07b790c6bc..93c8a227a8 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -16,10 +16,11 @@ from cura.Machines.MaterialNode import MaterialNode #For type checking. # so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc. # class MaterialGroup: - __slots__ = ("name", "root_material_node", "derived_material_node_list") + __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") def __init__(self, name: str, root_material_node: MaterialNode): self.name = name + self.is_read_only = False self.root_material_node = root_material_node self.derived_material_node_list = [] #type: List[MaterialNode] diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index b01d360ab6..0a82fcc764 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -86,6 +86,7 @@ class MaterialManager(QObject): root_material_id = material_metadata.get("base_file") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) + self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) group = self._material_group_map[root_material_id] #Store this material in the group of the appropriate root material. @@ -100,13 +101,6 @@ class MaterialManager(QObject): # GUID -> material group list self._guid_material_groups_map = defaultdict(list) for root_material_id, material_group in self._material_group_map.items(): - # This can happen when we are updating with incomplete data. - if material_group.root_material_node is None: - Logger.log("e", "Missing root material node for [%s]. Probably caused by update using incomplete data." - " Check all related signals for further debugging.", - material_group.name) - self._update_timer.start() - return guid = material_group.root_material_node.metadata["GUID"] self._guid_material_groups_map[guid].append(material_group) @@ -331,6 +325,35 @@ class MaterialManager(QObject): return material_node + # + # Gets MaterialNode for the given extruder and machine with the given material type. + # Returns None if: + # 1. the given machine doesn't have materials; + # 2. cannot find any material InstanceContainers with the given settings. + # + def getMaterialNodeByType(self, global_stack: "GlobalStack", extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]: + node = None + machine_definition = global_stack.definition + if parseBool(machine_definition.getMetaDataEntry("has_materials", False)): + material_diameter = machine_definition.getProperty("material_diameter", "value") + if isinstance(material_diameter, SettingFunction): + material_diameter = material_diameter(global_stack) + + # Look at the guid to material dictionary + root_material_id = None + for material_group in self._guid_material_groups_map[material_guid]: + if material_group.is_read_only: + root_material_id = material_group.root_material_node.metadata["id"] + break + + if not root_material_id: + Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) + return None + + node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, + material_diameter, root_material_id) + return node + # # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla". # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use diff --git a/cura/Machines/Models/BrandMaterialsModel.py b/cura/Machines/Models/BrandMaterialsModel.py index e36c6448d3..f6c9a14632 100644 --- a/cura/Machines/Models/BrandMaterialsModel.py +++ b/cura/Machines/Models/BrandMaterialsModel.py @@ -53,8 +53,8 @@ class BrandMaterialsModel(ListModel): self._extruder_manager = CuraApplication.getInstance().getExtruderManager() self._material_manager = CuraApplication.getInstance().getMaterialManager() - self._machine_manager.globalContainerChanged.connect(self._update) - self._material_manager.materialsUpdated.connect(self._update) + self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines. + self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes. self._update() def setExtruderPosition(self, position: int): diff --git a/cura/Machines/Models/GenericMaterialsModel.py b/cura/Machines/Models/GenericMaterialsModel.py index 6b149448ea..2fac919f3e 100644 --- a/cura/Machines/Models/GenericMaterialsModel.py +++ b/cura/Machines/Models/GenericMaterialsModel.py @@ -15,8 +15,8 @@ class GenericMaterialsModel(BaseMaterialsModel): self._extruder_manager = CuraApplication.getInstance().getExtruderManager() self._material_manager = CuraApplication.getInstance().getMaterialManager() - self._machine_manager.globalContainerChanged.connect(self._update) - self._material_manager.materialsUpdated.connect(self._update) + self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines. + self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes. self._update() def _update(self): diff --git a/cura/Machines/Models/MachineManagementModel.py b/cura/Machines/Models/MachineManagementModel.py new file mode 100644 index 0000000000..7dc51f07f7 --- /dev/null +++ b/cura/Machines/Models/MachineManagementModel.py @@ -0,0 +1,82 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Qt.ListModel import ListModel + +from PyQt5.QtCore import Qt + +from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.Settings.ContainerStack import ContainerStack + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + + +# +# This the QML model for the quality management page. +# +class MachineManagementModel(ListModel): + NameRole = Qt.UserRole + 1 + IdRole = Qt.UserRole + 2 + MetaDataRole = Qt.UserRole + 3 + GroupRole = Qt.UserRole + 4 + + def __init__(self, parent = None): + super().__init__(parent) + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.IdRole, "id") + self.addRoleName(self.MetaDataRole, "metadata") + self.addRoleName(self.GroupRole, "group") + self._local_container_stacks = [] + self._network_container_stacks = [] + + # Listen to changes + ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged) + ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged) + ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) + self._filter_dict = {} + self._update() + + ## 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) and container.getMetaDataEntry("type") == "machine": + self._update() + + ## Private convenience function to reset & repopulate the model. + def _update(self): + items = [] + + # Get first the network enabled printers + 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")) + + for container in self._network_container_stacks: + metadata = container.getMetaData().copy() + if container.getBottom(): + 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")}) + + # 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()) + + for container in self._local_container_stacks: + metadata = container.getMetaData().copy() + if container.getBottom(): + metadata["definition_name"] = container.getBottom().getName() + + items.append({"name": container.getName(), + "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/QualitySettingsModel.py b/cura/Machines/Models/QualitySettingsModel.py index 4470ffc80e..b38f6f65c8 100644 --- a/cura/Machines/Models/QualitySettingsModel.py +++ b/cura/Machines/Models/QualitySettingsModel.py @@ -87,9 +87,11 @@ class QualitySettingsModel(ListModel): if self._selected_position == self.GLOBAL_STACK_POSITION: quality_node = quality_group.node_for_global else: - quality_node = quality_group.nodes_for_extruders.get(self._selected_position) + quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position)) settings_keys = quality_group.getAllKeys() - quality_containers = [quality_node.getContainer()] + quality_containers = [] + if quality_node is not None: + quality_containers.append(quality_node.getContainer()) # Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch # the settings in that quality_changes_group. @@ -97,7 +99,7 @@ class QualitySettingsModel(ListModel): if self._selected_position == self.GLOBAL_STACK_POSITION: quality_changes_node = quality_changes_group.node_for_global else: - quality_changes_node = quality_changes_group.nodes_for_extruders.get(self._selected_position) + quality_changes_node = quality_changes_group.nodes_for_extruders.get(str(self._selected_position)) if quality_changes_node is not None: # it can be None if number of extruders are changed during runtime try: quality_containers.insert(0, quality_changes_node.getContainer()) diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index efb940b857..8d972c9192 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -16,6 +16,7 @@ from .QualityGroup import QualityGroup from .QualityNode import QualityNode if TYPE_CHECKING: + from UM.Settings.DefinitionContainer import DefinitionContainer from cura.Settings.GlobalStack import GlobalStack from .QualityChangesGroup import QualityChangesGroup @@ -178,7 +179,7 @@ class QualityManager(QObject): # Returns a dict of "custom profile name" -> QualityChangesGroup def getQualityChangesGroups(self, machine: "GlobalStack") -> dict: - machine_definition_id = getMachineDefinitionIDForQualitySearch(machine) + machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id) if not machine_node: @@ -206,7 +207,7 @@ class QualityManager(QObject): # For more details, see QualityGroup. # def getQualityGroups(self, machine: "GlobalStack") -> dict: - machine_definition_id = getMachineDefinitionIDForQualitySearch(machine) + machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False)) @@ -315,7 +316,7 @@ class QualityManager(QObject): return quality_group_dict def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict: - machine_definition_id = getMachineDefinitionIDForQualitySearch(machine) + machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) # To find the quality container for the GlobalStack, check in the following fall-back manner: # (1) the machine-specific node @@ -460,7 +461,7 @@ class QualityManager(QObject): quality_changes.addMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) # If the machine specifies qualities should be filtered, ensure we match the current criteria. - machine_definition_id = getMachineDefinitionIDForQualitySearch(machine) + machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) quality_changes.setDefinition(machine_definition_id) quality_changes.addMetaDataEntry("setting_version", self._application.SettingVersion) @@ -480,12 +481,13 @@ class QualityManager(QObject): # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended # shares the same set of qualities profiles as Ultimaker 3. # -def getMachineDefinitionIDForQualitySearch(machine: "GlobalStack", default_definition_id: str = "fdmprinter") -> str: +def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer", + default_definition_id: str = "fdmprinter") -> str: machine_definition_id = default_definition_id - if parseBool(machine.getMetaDataEntry("has_machine_quality", False)): + if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)): # Only use the machine's own quality definition ID if this machine has machine quality. - machine_definition_id = machine.getMetaDataEntry("quality_definition") + machine_definition_id = machine_definition.getMetaDataEntry("quality_definition") if machine_definition_id is None: - machine_definition_id = machine.definition.getId() + machine_definition_id = machine_definition.getId() return machine_definition_id diff --git a/cura/Machines/VariantManager.py b/cura/Machines/VariantManager.py index 1e6dcfe838..4e033e054e 100644 --- a/cura/Machines/VariantManager.py +++ b/cura/Machines/VariantManager.py @@ -25,7 +25,7 @@ ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE) # -# VariantManager is THE place to look for a specific variant. It maintains a variant lookup table with the following +# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following # structure: # # [machine_definition_id] -> [variant_type] -> [variant_name] -> ContainerNode(metadata / container) @@ -35,6 +35,9 @@ ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE) # -> "BB 0.8" # -> ... # +# [machine_definition_id] -> [machine_buildplate_type] -> ContainerNode(metadata / container) +# Example: "ultimaker3" -> "glass" (this is different from the variant name) -> ContainerNode +# # Note that the "container" field is not loaded in the beginning because it would defeat the purpose of lazy-loading. # A container is loaded when getVariant() is called to load a variant InstanceContainer. # @@ -44,6 +47,7 @@ class VariantManager: self._container_registry = container_registry # type: ContainerRegistry self._machine_to_variant_dict_map = dict() # -> + self._machine_to_buildplate_dict_map = dict() self._exclude_variant_id_list = ["empty_variant"] @@ -53,6 +57,7 @@ class VariantManager: # def initialize(self): self._machine_to_variant_dict_map = OrderedDict() + self._machine_to_buildplate_dict_map = OrderedDict() # Cache all variants from the container registry to a variant map for better searching and organization. variant_metadata_list = self._container_registry.findContainersMetadata(type = "variant") @@ -78,6 +83,22 @@ class VariantManager: variant_dict[variant_name] = ContainerNode(metadata = variant_metadata) + # If the variant is a buildplate then fill also the buildplate map + if variant_type == VariantType.BUILD_PLATE: + if variant_definition not in self._machine_to_buildplate_dict_map: + self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict() + + variant_container = self._container_registry.findContainers(type = "variant", id = variant_metadata["id"]) + if not variant_container: + # ERROR: not variant container. This should never happen + raise RuntimeError("Not variant found [%s], type [%s] for machine [%s]" % + (variant_name, variant_type, variant_definition)) + buildplate_type = variant_container[0].getProperty("machine_buildplate_type", "value") + if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]: + self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict() + + self._machine_to_buildplate_dict_map[variant_definition][buildplate_type] = variant_dict[variant_name] + # # Gets the variant InstanceContainer with the given information. # Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present. @@ -117,3 +138,8 @@ class VariantManager: if preferred_variant_name: node = self.getVariantNode(machine_definition_id, preferred_variant_name, variant_type) return node + + def getBuildplateVariantNode(self, machine_definition_id: str, buildplate_type: str) -> Optional["ContainerNode"]: + if machine_definition_id in self._machine_to_buildplate_dict_map: + return self._machine_to_buildplate_dict_map[machine_definition_id].get(buildplate_type) + return None 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/PlatformPhysics.py b/cura/PlatformPhysics.py index 69890178e4..3d9d5d5027 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -40,6 +40,8 @@ class PlatformPhysics: Preferences.getInstance().addPreference("physics/automatic_drop_down", True) def _onSceneChanged(self, source): + if not source.getMeshData(): + return self._change_timer.start() def _onChangeTimerFinished(self): diff --git a/cura/PrinterOutput/ConfigurationModel.py b/cura/PrinterOutput/ConfigurationModel.py new file mode 100644 index 0000000000..c03d968b9e --- /dev/null +++ b/cura/PrinterOutput/ConfigurationModel.py @@ -0,0 +1,81 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal +from typing import List + +MYPY = False +if MYPY: + from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel + + +class ConfigurationModel(QObject): + + configurationChanged = pyqtSignal() + + def __init__(self): + super().__init__() + self._printer_type = None + self._extruder_configurations = [] # type: List[ExtruderConfigurationModel] + self._buildplate_configuration = None + + def setPrinterType(self, printer_type): + self._printer_type = printer_type + + @pyqtProperty(str, fset = setPrinterType, notify = configurationChanged) + def printerType(self): + return self._printer_type + + def setExtruderConfigurations(self, extruder_configurations): + self._extruder_configurations = extruder_configurations + + @pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged) + def extruderConfigurations(self): + return self._extruder_configurations + + def setBuildplateConfiguration(self, buildplate_configuration): + self._buildplate_configuration = buildplate_configuration + + @pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged) + def buildplateConfiguration(self): + return self._buildplate_configuration + + ## This method is intended to indicate whether the configuration is valid or not. + # The method checks if the mandatory fields are or not set + def isValid(self): + if not self._extruder_configurations: + return False + for configuration in self._extruder_configurations: + if configuration is None: + return False + return self._printer_type is not None + + def __str__(self): + message_chunks = [] + message_chunks.append("Printer type: " + self._printer_type) + message_chunks.append("Extruders: [") + for configuration in self._extruder_configurations: + message_chunks.append(" " + str(configuration)) + message_chunks.append("]") + if self._buildplate_configuration is not None: + message_chunks.append("Buildplate: " + self._buildplate_configuration) + + return "\n".join(message_chunks) + + def __eq__(self, other): + return hash(self) == hash(other) + + ## The hash function is used to compare and create unique sets. The configuration is unique if the configuration + # of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. + def __hash__(self): + extruder_hash = hash(0) + first_extruder = None + for configuration in self._extruder_configurations: + extruder_hash ^= hash(configuration) + if configuration.position == 0: + first_extruder = configuration + # To ensure the correct order of the extruders, we add an "and" operation using the first extruder hash value + if first_extruder: + extruder_hash &= hash(first_extruder) + + return hash(self._printer_type) ^ extruder_hash ^ hash(self._buildplate_configuration) \ No newline at end of file diff --git a/cura/PrinterOutput/ExtruderConfigurationModel.py b/cura/PrinterOutput/ExtruderConfigurationModel.py new file mode 100644 index 0000000000..bc7f1a7c07 --- /dev/null +++ b/cura/PrinterOutput/ExtruderConfigurationModel.py @@ -0,0 +1,59 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal + + +class ExtruderConfigurationModel(QObject): + + extruderConfigurationChanged = pyqtSignal() + + def __init__(self): + super().__init__() + self._position = -1 + self._material = None + self._hotend_id = None + + def setPosition(self, position): + self._position = position + + @pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged) + def position(self): + return self._position + + def setMaterial(self, material): + self._material = material + + @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) + def material(self): + return self._material + + def setHotendID(self, hotend_id): + self._hotend_id = hotend_id + + @pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged) + def hotendID(self): + return self._hotend_id + + ## This method is intended to indicate whether the configuration is valid or not. + # The method checks if the mandatory fields are or not set + # At this moment is always valid since we allow to have empty material and variants. + def isValid(self): + return True + + def __str__(self): + message_chunks = [] + message_chunks.append("Position: " + str(self._position)) + message_chunks.append("-") + message_chunks.append("Material: " + self.material.type if self.material else "empty") + message_chunks.append("-") + message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty") + return " ".join(message_chunks) + + def __eq__(self, other): + return hash(self) == hash(other) + + # Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is + # unique within a set + def __hash__(self): + return hash(self._position) ^ (hash(self._material.guid) if self._material is not None else hash(0)) ^ hash(self._hotend_id) \ No newline at end of file diff --git a/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOutputModel.py similarity index 70% rename from cura/PrinterOutput/ExtruderOuputModel.py rename to cura/PrinterOutput/ExtruderOutputModel.py index b0be6cbbe4..e4c7f1608e 100644 --- a/cura/PrinterOutput/ExtruderOuputModel.py +++ b/cura/PrinterOutput/ExtruderOutputModel.py @@ -1,8 +1,8 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot -from UM.Logger import Logger +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot +from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from typing import Optional @@ -17,14 +17,18 @@ class ExtruderOutputModel(QObject): targetHotendTemperatureChanged = pyqtSignal() hotendTemperatureChanged = pyqtSignal() activeMaterialChanged = pyqtSignal() + extruderConfigurationChanged = pyqtSignal() - def __init__(self, printer: "PrinterOutputModel", parent=None): + def __init__(self, printer: "PrinterOutputModel", position, parent=None): super().__init__(parent) self._printer = printer + self._position = position self._target_hotend_temperature = 0 self._hotend_temperature = 0 self._hotend_id = "" self._active_material = None # type: Optional[MaterialOutputModel] + self._extruder_configuration = ExtruderConfigurationModel() + self._extruder_configuration.position = self._position @pyqtProperty(QObject, notify = activeMaterialChanged) def activeMaterial(self) -> "MaterialOutputModel": @@ -33,7 +37,9 @@ class ExtruderOutputModel(QObject): def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]): if self._active_material != material: self._active_material = material + self._extruder_configuration.material = self._active_material self.activeMaterialChanged.emit() + self.extruderConfigurationChanged.emit() ## Update the hotend temperature. This only changes it locally. def updateHotendTemperature(self, temperature: float): @@ -56,7 +62,7 @@ class ExtruderOutputModel(QObject): def targetHotendTemperature(self) -> float: return self._target_hotend_temperature - @pyqtProperty(float, notify=hotendTemperatureChanged) + @pyqtProperty(float, notify = hotendTemperatureChanged) def hotendTemperature(self) -> float: return self._hotend_temperature @@ -67,4 +73,12 @@ class ExtruderOutputModel(QObject): def updateHotendID(self, id: str): if self._hotend_id != id: self._hotend_id = id + self._extruder_configuration.hotendID = self._hotend_id self.hotendIDChanged.emit() + self.extruderConfigurationChanged.emit() + + @pyqtProperty(QObject, notify = extruderConfigurationChanged) + def extruderConfiguration(self): + if self._extruder_configuration.isValid(): + return self._extruder_configuration + return None diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 315b195e2a..eefbd9ae12 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -55,6 +55,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._connection_state_before_timeout = None # type: Optional[ConnectionState] + printer_type = self._properties.get(b"machine", b"").decode("utf-8") + printer_type_identifiers = { + "9066": "ultimaker3", + "9511": "ultimaker3_extended" + } + self._printer_type = "Unknown" + for key, value in printer_type_identifiers.items(): + if printer_type.startswith(key): + self._printer_type = value + break + def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None: raise NotImplementedError("requestWrite needs to be implemented") @@ -301,6 +312,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def firmwareVersion(self) -> str: return self._properties.get(b"firmware_version", b"").decode("utf-8") + @pyqtProperty(str, constant=True) + def printerType(self) -> str: + return self._printer_type + ## IPadress of this printer @pyqtProperty(str, constant=True) def ipAddress(self) -> str: diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 86ca10e2d3..1d658e79be 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -6,7 +6,7 @@ from UM.Logger import Logger MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel - from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel + from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel @@ -18,7 +18,7 @@ class PrinterOutputController: self.can_control_manually = True self._output_device = output_device - def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int): + def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOutputModel", temperature: int): Logger.log("w", "Set target hotend temperature not implemented in controller") def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 1c23d0e18e..712f9b5b1e 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -1,11 +1,11 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot -from UM.Logger import Logger -from typing import Optional, List +from typing import Optional from UM.Math.Vector import Vector -from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel +from cura.PrinterOutput.ConfigurationModel import ConfigurationModel +from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel MYPY = False if MYPY: @@ -22,8 +22,10 @@ class PrinterOutputModel(QObject): nameChanged = pyqtSignal() headPositionChanged = pyqtSignal() keyChanged = pyqtSignal() - typeChanged = pyqtSignal() + printerTypeChanged = pyqtSignal() + buildplateChanged = pyqtSignal() cameraChanged = pyqtSignal() + configurationChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""): super().__init__(parent) @@ -32,13 +34,18 @@ class PrinterOutputModel(QObject): self._name = "" self._key = "" # Unique identifier self._controller = output_controller - self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)] + self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] + self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] self._firmware_version = firmware_version self._printer_state = "unknown" self._is_preheating = False - self._type = "" + self._printer_type = "" + self._buildplate_name = None + # Update the printer configuration every time any of the extruders changes its configuration + for extruder in self._extruders: + extruder.extruderConfigurationChanged.connect(self._updateExtruderConfiguration) self._camera = None @@ -64,14 +71,27 @@ class PrinterOutputModel(QObject): def camera(self): return self._camera - @pyqtProperty(str, notify = typeChanged) + @pyqtProperty(str, notify = printerTypeChanged) def type(self): - return self._type + return self._printer_type - def updateType(self, type): - if self._type != type: - self._type = type - self.typeChanged.emit() + def updateType(self, printer_type): + if self._printer_type != printer_type: + self._printer_type = printer_type + self._printer_configuration.printerType = self._printer_type + self.printerTypeChanged.emit() + self.configurationChanged.emit() + + @pyqtProperty(str, notify = buildplateChanged) + def buildplate(self): + return self._buildplate_name + + def updateBuildplateName(self, buildplate_name): + if self._buildplate_name != buildplate_name: + self._buildplate_name = buildplate_name + self._printer_configuration.buildplateConfiguration = self._buildplate_name + self.buildplateChanged.emit() + self.configurationChanged.emit() @pyqtProperty(str, notify=keyChanged) def key(self): @@ -238,3 +258,14 @@ class PrinterOutputModel(QObject): if self._controller: return self._controller.can_control_manually return False + + # Returns the configuration (material, variant and buildplate) of the current printer + @pyqtProperty(QObject, notify = configurationChanged) + def printerConfiguration(self): + if self._printer_configuration.isValid(): + return self._printer_configuration + return None + + def _updateExtruderConfiguration(self): + self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders] + self.configurationChanged.emit() diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 9e603b83ae..4d6ddb8dfa 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -1,12 +1,11 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.i18n import i18nCatalog from UM.OutputDevice.OutputDevice import OutputDevice -from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal +from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal, QVariant from PyQt5.QtWidgets import QMessageBox - from UM.Logger import Logger from UM.Signal import signalemitter from UM.Application import Application @@ -17,6 +16,7 @@ from typing import List, Optional MYPY = False if MYPY: from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + from cura.PrinterOutput.ConfigurationModel import ConfigurationModel i18n_catalog = i18nCatalog("cura") @@ -44,10 +44,14 @@ class PrinterOutputDevice(QObject, OutputDevice): # Signal to indicate that the info text about the connection has changed. connectionTextChanged = pyqtSignal() + # Signal to indicate that the configuration of one of the printers has changed. + uniqueConfigurationsChanged = pyqtSignal() + def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) self._printers = [] # type: List[PrinterOutputModel] + self._unique_configurations = [] # type: List[ConfigurationModel] self._monitor_view_qml_path = "" self._monitor_component = None @@ -69,6 +73,8 @@ class PrinterOutputDevice(QObject, OutputDevice): self._address = "" self._connection_text = "" + self.printersChanged.connect(self._onPrintersChanged) + Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations) @pyqtProperty(str, notify = connectionTextChanged) def address(self): @@ -175,6 +181,23 @@ class PrinterOutputDevice(QObject, OutputDevice): self.acceptsCommandsChanged.emit() + # Returns the unique configurations of the printers within this output device + @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged) + def uniqueConfigurations(self): + return self._unique_configurations + + def _updateUniqueConfigurations(self): + self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None])) + self._unique_configurations.sort(key = lambda k: k.printerType) + self.uniqueConfigurationsChanged.emit() + + def _onPrintersChanged(self): + for printer in self._printers: + printer.configurationChanged.connect(self._updateUniqueConfigurations) + + # At this point there may be non-updated configurations + self._updateUniqueConfigurations() + ## The current processing state of the backend. class ConnectionState(IntEnum): 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/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 828897b4dd..0cf1c7399f 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -204,7 +204,7 @@ class CuraContainerRegistry(ContainerRegistry): global_profile = profile_or_list[0] else: for profile in profile_or_list: - if not profile.getMetaDataEntry("extruder"): + if not profile.getMetaDataEntry("position"): global_profile = profile break if not global_profile: @@ -212,16 +212,34 @@ class CuraContainerRegistry(ContainerRegistry): return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "This profile {0} contains incorrect data, could not import it.", file_name)} profile_definition = global_profile.getMetaDataEntry("definition") - expected_machine_definition = "fdmprinter" - if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")): - expected_machine_definition = global_container_stack.getMetaDataEntry("quality_definition") - if not expected_machine_definition: - expected_machine_definition = global_container_stack.definition.getId() - if expected_machine_definition is not None and profile_definition is not None and profile_definition != expected_machine_definition: + + # Make sure we have a profile_definition in the file: + if profile_definition is None: + break + machine_definition = self.findDefinitionContainers(id = profile_definition) + if not machine_definition: + Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) + return {"status": "error", + "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "This profile {0} contains incorrect data, could not import it.", file_name) + } + machine_definition = machine_definition[0] + + # 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) + + # And check if the profile_definition matches either one (showing error if not): + if profile_definition != expected_machine_definition: Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition) return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "The machine defined in profile {0} ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)} + # Fix the global quality profile's definition field in case it's not correct + global_profile.setMetaDataEntry("definition", expected_machine_definition) + quality_name = global_profile.getName() + quality_type = global_profile.getMetaDataEntry("quality_type") + name_seed = os.path.splitext(os.path.basename(file_name))[0] new_name = self.uniqueName(name_seed) @@ -236,11 +254,11 @@ class CuraContainerRegistry(ContainerRegistry): for idx, extruder in enumerate(global_container_stack.extruders.values()): profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1)) profile = InstanceContainer(profile_id) - profile.setName(global_profile.getName()) + profile.setName(quality_name) profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) profile.addMetaDataEntry("type", "quality_changes") - profile.addMetaDataEntry("definition", global_profile.getMetaDataEntry("definition")) - profile.addMetaDataEntry("quality_type", global_profile.getMetaDataEntry("quality_type")) + profile.addMetaDataEntry("definition", expected_machine_definition) + profile.addMetaDataEntry("quality_type", quality_type) profile.addMetaDataEntry("position", "0") profile.setDirty(True) if idx == 0: @@ -273,17 +291,17 @@ class CuraContainerRegistry(ContainerRegistry): elif profile_index < len(machine_extruders) + 1: # This is assumed to be an extruder profile extruder_id = machine_extruders[profile_index - 1].definition.getId() - extuder_position = str(profile_index - 1) + extruder_position = str(profile_index - 1) if not profile.getMetaDataEntry("position"): - profile.addMetaDataEntry("position", extuder_position) + profile.addMetaDataEntry("position", extruder_position) else: - profile.setMetaDataEntry("position", extuder_position) + profile.setMetaDataEntry("position", extruder_position) profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") else: #More extruders in the imported file than in the machine. continue #Delete the additional profiles. - result = self._configureProfile(profile, profile_id, new_name) + result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) if result is not None: return {"status": "error", "message": catalog.i18nc( "@info:status Don't translate the XML tags or !", @@ -311,7 +329,7 @@ class CuraContainerRegistry(ContainerRegistry): # \param new_name The new name for the profile. # # \return None if configuring was successful or an error message if an error occurred. - def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]: + def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: profile.setDirty(True) # Ensure the profiles are correctly saved new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) @@ -321,6 +339,7 @@ class CuraContainerRegistry(ContainerRegistry): # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile # It also solves an issue with importing profiles from G-Codes profile.setMetaDataEntry("id", new_id) + profile.setMetaDataEntry("definition", machine_definition_id) if "type" in profile.getMetaData(): profile.setMetaDataEntry("type", "quality_changes") @@ -331,9 +350,8 @@ class CuraContainerRegistry(ContainerRegistry): if not quality_type: return catalog.i18nc("@info:status", "Profile is missing a quality type.") - quality_type_criteria = {"quality_type": quality_type} global_stack = Application.getInstance().getGlobalContainerStack() - definition_id = getMachineDefinitionIDForQualitySearch(global_stack) + definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition) profile.setDefinition(definition_id) # Check to make sure the imported profile actually makes sense in context of the current configuration. diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 0ac3e4bd66..2b422ec406 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -668,6 +668,8 @@ class ExtruderManager(QObject): # global stack if not found. @staticmethod def getExtruderValue(extruder_index, key): + if extruder_index == -1: + extruder_index = int(Application.getInstance().getMachineManager().defaultExtruderPosition) extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index) if extruder: diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index d79130e0c3..c79d352dcb 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -4,7 +4,7 @@ import collections import time #Type hinting. -from typing import Union, List, Dict, TYPE_CHECKING, Optional +from typing import List, Dict, TYPE_CHECKING, Optional from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Signal import Signal @@ -20,13 +20,15 @@ from UM.Logger import Logger from UM.Message import Message from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Settings.InstanceContainer import InstanceContainer 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 +from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from cura.Settings.ExtruderManager import ExtruderManager from .CuraStackBuilder import CuraStackBuilder @@ -48,7 +50,6 @@ class MachineManager(QObject): self._global_container_stack = None # type: GlobalStack self._current_root_material_id = {} - self._current_root_material_name = {} self._current_quality_group = None self._current_quality_changes_group = None @@ -56,11 +57,6 @@ class MachineManager(QObject): self.machine_extruder_material_update_dict = collections.defaultdict(list) - self._error_check_timer = QTimer() - self._error_check_timer.setInterval(250) - self._error_check_timer.setSingleShot(True) - self._error_check_timer.timeout.connect(self._updateStacksHaveErrors) - self._instance_container_timer = QTimer() self._instance_container_timer.setInterval(250) self._instance_container_timer.setSingleShot(True) @@ -109,6 +105,12 @@ class MachineManager(QObject): # There might already be some output devices by the time the signal is connected self._onOutputDevicesChanged() + self._current_printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer + self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged) + self.activeVariantChanged.connect(self._onCurrentConfigurationChanged) + # Force to compute the current configuration + self._onCurrentConfigurationChanged() + self._application.callLater(self.setInitialActiveMachine) self._material_incompatible_message = Message(catalog.i18nc("@info:status", @@ -119,12 +121,17 @@ class MachineManager(QObject): if containers: containers[0].nameChanged.connect(self._onMaterialNameChanged) - self._material_manager = self._application._material_manager + self._material_manager = self._application.getMaterialManager() + self._variant_manager = self._application.getVariantManager() self._quality_manager = self._application.getQualityManager() # When the materials lookup table gets updated, it can mean that a material has its name changed, which should # be reflected on the GUI. This signal emission makes sure that it happens. self._material_manager.materialsUpdated.connect(self.rootMaterialChanged) + # When the materials get updated, it can be that an activated material's diameter gets changed. In that case, + # a material update should be triggered to make sure that the machine still has compatible materials activated. + self._material_manager.materialsUpdated.connect(self._updateUponMaterialMetadataChange) + self.rootMaterialChanged.connect(self._onRootMaterialChanged) activeQualityGroupChanged = pyqtSignal() activeQualityChangesGroupChanged = pyqtSignal() @@ -144,6 +151,7 @@ class MachineManager(QObject): blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly outputDevicesChanged = pyqtSignal() + currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes rootMaterialChanged = pyqtSignal() @@ -163,6 +171,39 @@ class MachineManager(QObject): self.outputDevicesChanged.emit() + @pyqtProperty(QObject, notify = currentConfigurationChanged) + def currentConfiguration(self): + return self._current_printer_configuration + + def _onCurrentConfigurationChanged(self) -> None: + if not self._global_container_stack: + return + + # Create the configuration model with the current data in Cura + self._current_printer_configuration.printerType = self._global_container_stack.definition.getName() + self._current_printer_configuration.extruderConfigurations = [] + for extruder in self._global_container_stack.extruders.values(): + extruder_configuration = ExtruderConfigurationModel() + # For compare just the GUID is needed at this moment + mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != self._empty_material_container else None + mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != self._empty_material_container else None + mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != self._empty_material_container else None + mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != self._empty_material_container else None + mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != self._empty_material_container else None + material_model = MaterialOutputModel(mat_guid, mat_type, mat_color, mat_brand, mat_name) + + extruder_configuration.position = int(extruder.getMetaDataEntry("position")) + extruder_configuration.material = material_model + extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != self._empty_variant_container else None + self._current_printer_configuration.extruderConfigurations.append(extruder_configuration) + + self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != self._empty_variant_container else None + self.currentConfigurationChanged.emit() + + @pyqtSlot(QObject, result = bool) + def matchesConfiguration(self, configuration: ConfigurationModel) -> bool: + return self._current_printer_configuration == configuration + @pyqtProperty("QVariantList", notify = outputDevicesChanged) def printerOutputDevices(self): return self._printer_output_devices @@ -227,15 +268,6 @@ class MachineManager(QObject): del self.machine_extruder_material_update_dict[self._global_container_stack.getId()] self.activeQualityGroupChanged.emit() - self._error_check_timer.start() - - ## Update self._stacks_valid according to _checkStacksForErrors and emit if change. - def _updateStacksHaveErrors(self) -> None: - old_stacks_have_errors = self._stacks_have_errors - self._stacks_have_errors = self._checkStacksHaveErrors() - if old_stacks_have_errors != self._stacks_have_errors: - self.stacksValidationChanged.emit() - Application.getInstance().stacksValidationFinished.emit() def _onActiveExtruderStackChanged(self) -> None: self.blurSettings.emit() # Ensure no-one has focus. @@ -255,8 +287,6 @@ class MachineManager(QObject): self.rootMaterialChanged.emit() - self._error_check_timer.start() - def _onInstanceContainersChanged(self, container) -> None: self._instance_container_timer.start() @@ -265,9 +295,6 @@ class MachineManager(QObject): # Notify UI items, such as the "changed" star in profile pull down menu. self.activeStackValueChanged.emit() - elif property_name == "validationState": - self._error_check_timer.start() - ## Given a global_stack, make sure that it's all valid by searching for this quality group and applying it again def _initMachineState(self, global_stack): material_dict = {} @@ -311,9 +338,13 @@ class MachineManager(QObject): self.__emitChangedSignals() + ## Given a definition id, return the machine with this id. + # Optional: add a list of keys and values to filter the list of machines with the given definition id + # \param definition_id \type{str} definition id that needs to look for + # \param metadata_filter \type{dict} list of metadata keys and values used for filtering @staticmethod - def getMachine(definition_id: str) -> Optional["GlobalStack"]: - machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine") + def getMachine(definition_id: str, metadata_filter: Dict[str, str] = None) -> Optional["GlobalStack"]: + machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) for machine in machines: if machine.definition.getId() == definition_id: return machine @@ -334,7 +365,7 @@ class MachineManager(QObject): return False if self._global_container_stack.hasErrors(): - Logger.log("d", "Checking global stack for errors took %0.2f s and we found and error" % (time.time() - time_start)) + Logger.log("d", "Checking global stack for errors took %0.2f s and we found an error" % (time.time() - time_start)) return True # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are @@ -418,6 +449,12 @@ class MachineManager(QObject): def stacksHaveErrors(self) -> bool: return bool(self._stacks_have_errors) + @pyqtProperty(str, notify = globalContainerChanged) + def activeMachineDefinitionName(self) -> str: + if self._global_container_stack: + return self._global_container_stack.definition.getName() + return "" + @pyqtProperty(str, notify = globalContainerChanged) def activeMachineName(self) -> str: if self._global_container_stack: @@ -430,6 +467,18 @@ class MachineManager(QObject): return self._global_container_stack.getId() return "" + @pyqtProperty(str, notify = outputDevicesChanged) + def activeMachineNetworkKey(self) -> str: + if self._global_container_stack: + return self._global_container_stack.getMetaDataEntry("um_network_key") + return "" + + @pyqtProperty(str, notify = outputDevicesChanged) + def activeMachineNetworkGroupName(self) -> str: + if self._global_container_stack: + return self._global_container_stack.getMetaDataEntry("connect_group_name") + return "" + @pyqtProperty(QObject, notify = globalContainerChanged) def activeMachine(self) -> Optional["GlobalStack"]: return self._global_container_stack @@ -579,7 +628,7 @@ class MachineManager(QObject): @pyqtProperty(str, notify = globalContainerChanged) def activeQualityDefinitionId(self) -> str: if self._global_container_stack: - return getMachineDefinitionIDForQualitySearch(self._global_container_stack) + return getMachineDefinitionIDForQualitySearch(self._global_container_stack.definition) return "" ## Gets how the active definition calls variants @@ -830,10 +879,13 @@ class MachineManager(QObject): return self._default_extruder_position ## This will fire the propertiesChanged for all settings so they will be updated in the front-end + @pyqtSlot() def forceUpdateAllSettings(self): - property_names = ["value", "resolve"] - for setting_key in self._global_container_stack.getAllKeys(): - self._global_container_stack.propertiesChanged.emit(setting_key, property_names) + with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): + property_names = ["value", "resolve", "validationState"] + for container in [self._global_container_stack] + list(self._global_container_stack.extruders.values()): + for setting_key in container.getAllKeys(): + container.propertiesChanged.emit(setting_key, property_names) @pyqtSlot(int, bool) def setExtruderEnabled(self, position: int, enabled) -> None: @@ -874,27 +926,22 @@ class MachineManager(QObject): @pyqtProperty("QVariantList", notify = globalContainerChanged) def currentExtruderPositions(self): + if self._global_container_stack is None: + return [] return sorted(list(self._global_container_stack.extruders.keys())) + ## Update _current_root_material_id when the current root material was changed. + def _onRootMaterialChanged(self): + self._current_root_material_id = {} + + if self._global_container_stack: + for position in self._global_container_stack.extruders: + self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file") + @pyqtProperty("QVariant", notify = rootMaterialChanged) def currentRootMaterialId(self): - # initial filling the current_root_material_id - self._current_root_material_id = {} - for position in self._global_container_stack.extruders: - self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file") return self._current_root_material_id - @pyqtProperty("QVariant", notify = rootMaterialChanged) - def currentRootMaterialName(self): - # initial filling the current_root_material_name - if self._global_container_stack: - self._current_root_material_name = {} - for position in self._global_container_stack.extruders: - if position not in self._current_root_material_name: - material = self._global_container_stack.extruders[position].material - self._current_root_material_name[position] = material.getName() - return self._current_root_material_name - ## Return the variant names in the extruder stack(s). ## For the variant in the global stack, use activeVariantBuildplateName @pyqtProperty("QVariant", notify = activeVariantChanged) @@ -991,15 +1038,12 @@ class MachineManager(QObject): if container_node: self._global_container_stack.extruders[position].material = container_node.getContainer() root_material_id = container_node.metadata["base_file"] - root_material_name = container_node.getContainer().getName() else: self._global_container_stack.extruders[position].material = self._empty_material_container root_material_id = None - root_material_name = None # The _current_root_material_id is used in the MaterialMenu to see which material is selected if root_material_id != self._current_root_material_id[position]: self._current_root_material_id[position] = root_material_id - self._current_root_material_name[position] = root_material_name self.rootMaterialChanged.emit() def activeMaterialsCompatible(self): @@ -1013,7 +1057,7 @@ class MachineManager(QObject): return True ## Update current quality type and machine after setting material - def _updateQualityWithMaterial(self): + def _updateQualityWithMaterial(self, *args): Logger.log("i", "Updating quality/quality_changes due to material change") current_quality_type = None if self._current_quality_group: @@ -1058,9 +1102,15 @@ class MachineManager(QObject): extruder = self._global_container_stack.extruders[position] current_material_base_name = extruder.material.getMetaDataEntry("base_file") - current_variant_name = extruder.variant.getMetaDataEntry("name") + current_variant_name = None + if extruder.variant.getId() != self._empty_variant_container.getId(): + current_variant_name = extruder.variant.getMetaDataEntry("name") - material_diameter = self._global_container_stack.getProperty("material_diameter", "value") + from UM.Settings.Interfaces import PropertyEvaluationContext + 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) candidate_materials = self._material_manager.getAvailableMaterials( self._global_container_stack.definition.getId(), current_variant_name, @@ -1075,6 +1125,74 @@ class MachineManager(QObject): self._setMaterial(position, new_material) continue + # The current material is not available, find the preferred one + material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, current_variant_name) + if material_node is not None: + self._setMaterial(position, material_node) + + ## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new + # instance with the same network key. + @pyqtSlot(str) + def switchPrinterType(self, machine_name): + # Don't switch if the user tries to change to the same type of printer + if self.activeMachineDefinitionName == machine_name: + return + # Get the definition id corresponding to this machine name + machine_definition_id = ContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId() + # Try to find a machine with the same network key + new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey}) + # If there is no machine, then create a new one and set it to the non-hidden instance + if not new_machine: + new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id) + new_machine.addMetaDataEntry("um_network_key", self.activeMachineNetworkKey) + new_machine.addMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName) + new_machine.addMetaDataEntry("hidden", False) + else: + Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey) + new_machine.setMetaDataEntry("hidden", False) + + # Set the current printer instance to hidden (the metadata entry must exist) + self._global_container_stack.setMetaDataEntry("hidden", True) + + self.setActiveMachine(new_machine.getId()) + + @pyqtSlot(QObject) + def applyRemoteConfiguration(self, configuration: ConfigurationModel): + self.blurSettings.emit() + with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): + self.switchPrinterType(configuration.printerType) + for extruder_configuration in configuration.extruderConfigurations: + position = str(extruder_configuration.position) + variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID) + material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack, extruder_configuration.hotendID,extruder_configuration.material.guid) + if variant_container_node: + self._setVariantNode(position, variant_container_node) + else: + self._global_container_stack.extruders[position].variant = self._empty_variant_container + + if material_container_node: + self._setMaterial(position, material_container_node) + else: + self._global_container_stack.extruders[position].material = self._empty_material_container + self._updateMaterialWithVariant(position) + + if configuration.buildplateConfiguration is not None: + global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration) + if global_variant_container_node: + self._setGlobalVariant(global_variant_container_node) + else: + self._global_container_stack.variant = self._empty_variant_container + else: + self._global_container_stack.variant = self._empty_variant_container + self._updateQualityWithMaterial() + + ## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value' + def replaceContainersMetadata(self, key: str, value: str, new_value: str): + machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine") + for machine in machines: + if machine.getMetaDataEntry(key) == value: + machine.setMetaDataEntry(key, new_value) + @pyqtSlot("QVariant") def setGlobalVariant(self, container_node): self.blurSettings.emit() @@ -1136,3 +1254,8 @@ class MachineManager(QObject): elif self._current_quality_group: name = self._current_quality_group.name return name + + def _updateUponMaterialMetadataChange(self): + with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): + self._updateMaterialWithVariant(None) + self._updateQualityWithMaterial() diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 0d4cd02cdb..e317b20f68 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -1,7 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal +from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal from UM.FlameProfiler import pyqtSlot from UM.Application import Application from UM.Logger import Logger @@ -30,6 +30,11 @@ class SettingInheritanceManager(QObject): ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged) self._onActiveExtruderChanged() + self._update_timer = QTimer() + self._update_timer.setInterval(500) + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._update) + settingsWithIntheritanceChanged = pyqtSignal() ## Get the keys of all children settings with an override. @@ -226,9 +231,7 @@ class SettingInheritanceManager(QObject): self._onActiveExtruderChanged() def _onContainersChanged(self, container): - # TODO: Multiple container changes in sequence now cause quite a few recalculations. - # This isn't that big of an issue, but it could be in the future. - self._update() + self._update_timer.start() @staticmethod def createSettingInheritanceManager(engine=None, script_engine=None): diff --git a/cura/Settings/SettingVisibilityPresetsModel.py b/cura/Settings/SettingVisibilityPresetsModel.py new file mode 100644 index 0000000000..e5a2e24412 --- /dev/null +++ b/cura/Settings/SettingVisibilityPresetsModel.py @@ -0,0 +1,136 @@ +# 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/Snapshot.py b/cura/Snapshot.py index 2a2a49d6cf..1f2a24aecd 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -66,7 +66,7 @@ class Snapshot: size = max(bbox.width, bbox.height, bbox.depth * 0.5) # Looking from this direction (x, y, z) in OGL coordinates - looking_from_offset = Vector(1, 1, 2) + looking_from_offset = Vector(-1, 1, 2) if size > 0: # determine the watch distance depending on the size looking_from_offset = looking_from_offset * size * 1.3 diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index ec590a0212..3a1298bdba 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -122,7 +122,7 @@ class ThreeMFReader(MeshReader): um_node.callDecoration("setActiveExtruder", default_stack.getId()) # Get the definition & set it - definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack) + definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition) um_node.callDecoration("getStack").getTop().setDefinition(definition_id) setting_container = um_node.callDecoration("getStack").getTop() diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 3c627a7655..f5daa77bb0 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -265,13 +265,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for material_container_file in material_container_files: container_id = self._stripFileToId(material_container_file) - from hashlib import sha1 - hex_container_id = sha1(container_id.encode('utf-8')).hexdigest() - serialized = archive.open(material_container_file).read().decode("utf-8") - metadata_list = xml_material_profile.deserializeMetadata(serialized, hex_container_id) - reverse_map = {metadata["id"].replace(hex_container_id, container_id): container_id.replace(hex_container_id, container_id) - for metadata in metadata_list} + metadata_list = xml_material_profile.deserializeMetadata(serialized, container_id) + reverse_map = {metadata["id"]: container_id for metadata in metadata_list} reverse_material_id_dict.update(reverse_map) material_labels.append(self._getMaterialLabelFromSerialized(serialized)) @@ -598,7 +594,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged") else: global_preferences.setValue("general/visible_settings", visible_settings) - global_preferences.setValue("general/preset_setting_visibility_choice", "Custom") + global_preferences.setValue("cura/active_setting_visibility_preset", "custom") categories_expanded = temp_preferences.getValue("cura/categories_expanded") if categories_expanded is None: @@ -723,7 +719,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Get the correct extruder definition IDs for quality changes from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch - machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(global_stack) + machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(global_stack.definition) machine_definition_for_quality = self._container_registry.findDefinitionContainers(id = machine_definition_id_for_quality)[0] quality_changes_info = self._machine_info.quality_changes_info @@ -754,15 +750,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_containers = self._container_registry.findInstanceContainers(name = quality_changes_name, type = "quality_changes") for container in quality_changes_containers: - extruder_definition_id = container.getMetaDataEntry("extruder") - if not extruder_definition_id: + extruder_position = container.getMetaDataEntry("position") + if extruder_position is None: quality_changes_info.global_info.container = container else: - extruder_definition_metadata = self._container_registry.findDefinitionContainersMetadata(id = extruder_definition_id)[0] - position = extruder_definition_metadata["position"] - if position not in quality_changes_info.extruder_info_dict: - quality_changes_info.extruder_info_dict[position] = ContainerInfo(None, None, None) - container_info = quality_changes_info.extruder_info_dict[position] + if extruder_position not in quality_changes_info.extruder_info_dict: + quality_changes_info.extruder_info_dict[extruder_position] = ContainerInfo(None, None, None) + container_info = quality_changes_info.extruder_info_dict[extruder_position] container_info.container = container # If there is no quality changes for any extruder, create one. diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 2f57e634e0..af6162c8d5 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -10,7 +10,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources -from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then. from UM.Platform import Platform from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Qt.Duration import DurationFormat @@ -32,6 +31,7 @@ import Arcus from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") + class CuraEngineBackend(QObject, Backend): backendError = Signal() @@ -62,23 +62,26 @@ class CuraEngineBackend(QObject, Backend): default_engine_location = execpath break + self._application = Application.getInstance() + self._multi_build_plate_model = None + self._machine_error_checker = None + if not default_engine_location: raise EnvironmentError("Could not find CuraEngine") - Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location)) + Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) Preferences.getInstance().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False - Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) - Application.getInstance().getMultiBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() + self._stored_layer_data = [] self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob - self._scene = Application.getInstance().getController().getScene() + self._scene = self._application.getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -86,20 +89,10 @@ class CuraEngineBackend(QObject, Backend): # - whenever there is a value change, we start the timer # - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the # auto-slicing timer when that error check is finished - # If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer, - # and only wait for the error check to be finished to start the auto-slicing timer again. + # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished + # to start the auto-slicing timer again. # self._global_container_stack = None - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - self._onGlobalStackChanged() - - Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) - # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash - ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged) - - # A flag indicating if an error check was scheduled - # If so, we will stop the auto-slice timer and start upon the error check - self._is_error_check_scheduled = False # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage @@ -125,13 +118,6 @@ class CuraEngineBackend(QObject, Backend): self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed self._postponed_scene_change_sources = [] # scene change is postponed (by a tool) - self.backendQuit.connect(self._onBackendQuit) - self.backendConnected.connect(self._onBackendConnected) - - # When a tool operation is in progress, don't slice. So we need to listen for tool operations. - Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted) - Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped) - self._slice_start_time = None Preferences.getInstance().addPreference("general/auto_slice", True) @@ -146,6 +132,30 @@ class CuraEngineBackend(QObject, Backend): self.determineAutoSlicing() Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + self._application.initializationFinished.connect(self.initialize) + + def initialize(self): + self._multi_build_plate_model = self._application.getMultiBuildPlateModel() + + self._application.getController().activeViewChanged.connect(self._onActiveViewChanged) + self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged) + + self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._onGlobalStackChanged() + + # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash + ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged) + + self.backendQuit.connect(self._onBackendQuit) + self.backendConnected.connect(self._onBackendConnected) + + # When a tool operation is in progress, don't slice. So we need to listen for tool operations. + self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted) + self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped) + + self._machine_error_checker = self._application.getMachineErrorChecker() + self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished) + ## Terminate the engine process. # # This function should terminate the engine process. @@ -531,11 +541,9 @@ class CuraEngineBackend(QObject, Backend): elif property == "validationState": if self._use_timer: - self._is_error_check_scheduled = True self._change_timer.stop() def _onStackErrorCheckFinished(self): - self._is_error_check_scheduled = False if not self._slicing and self._build_plates_to_be_sliced: self.needsSlicing() self._onChanged() @@ -561,12 +569,15 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) - # testing def _invokeSlice(self): if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual - if self._is_error_check_scheduled: + if self._machine_error_checker is None: + self._change_timer.stop() + return + + if self._machine_error_checker.needToWaitForResult: self._change_timer.stop() else: self._change_timer.start() @@ -632,7 +643,11 @@ class CuraEngineBackend(QObject, Backend): if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual - if self._is_error_check_scheduled: + if self._machine_error_checker is None: + self._change_timer.stop() + return + + if self._machine_error_checker.needToWaitForResult: self._change_timer.stop() else: self._change_timer.start() @@ -786,7 +801,7 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.start() def _extruderChanged(self): - for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1): + for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) self._invokeSlice() diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index f3f34f4c3d..96124a3514 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -281,7 +281,7 @@ class StartSliceJob(Job): default_extruder_position = int(Application.getInstance().getMachineManager().defaultExtruderPosition) result = {} for key in stack.getAllKeys(): - setting_type = stack.getProperty(key, "type") + setting_type = stack.definition.getProperty(key, "type") value = stack.getProperty(key, "value") if setting_type == "extruder" and value == -1: # replace with the default value diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index f941ef87b4..b12f8f8696 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -382,6 +382,11 @@ Cura.MachineAction property string settingKey: "machine_nozzle_size" property string label: catalog.i18nc("@label", "Nozzle size") property string unit: catalog.i18nc("@label", "mm") + function afterOnEditingFinished() + { + // Somehow the machine_nozzle_size dependent settings are not updated otherwise + Cura.MachineManager.forceUpdateAllSettings() + } property bool isExtruderSetting: true } @@ -889,4 +894,4 @@ Cura.MachineAction watchedProperties: [ "value" ] storeIndex: manager.containerIndex } -} \ No newline at end of file +} diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 1a1d37cbdf..ed84a8d2ce 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -22,14 +22,7 @@ class MonitorStage(CuraStage): def _setActivePrintJob(self, print_job): if self._active_print_job != print_job: - if self._active_print_job: - self._active_print_job.stateChanged.disconnect(self._updateIconSource) self._active_print_job = print_job - if self._active_print_job: - self._active_print_job.stateChanged.connect(self._updateIconSource) - - # Ensure that the right icon source is returned. - self._updateIconSource() def _setActivePrinter(self, printer): if self._active_printer != printer: @@ -43,9 +36,6 @@ class MonitorStage(CuraStage): else: self._setActivePrintJob(None) - # Ensure that the right icon source is returned. - self._updateIconSource() - def _onActivePrintJobChanged(self): self._setActivePrintJob(self._active_printer.activePrintJob) @@ -58,22 +48,13 @@ class MonitorStage(CuraStage): new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] if new_output_device != self._printer_output_device: if self._printer_output_device: - self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource) - self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource) self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged) self._printer_output_device = new_output_device - self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource) self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) - self._printer_output_device.connectionStateChanged.connect(self._updateIconSource) self._setActivePrinter(self._printer_output_device.activePrinter) - - # Force an update of the icon source - self._updateIconSource() except IndexError: - #If index error occurs, then the icon on monitor button also should be updated - self._updateIconSource() pass def _onEngineCreated(self): @@ -82,7 +63,6 @@ class MonitorStage(CuraStage): self._onOutputDevicesChanged() self._updateMainOverlay() self._updateSidebar() - self._updateIconSource() def _updateMainOverlay(self): main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml") @@ -92,46 +72,3 @@ class MonitorStage(CuraStage): # TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor! sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml") self.addDisplayComponent("sidebar", sidebar_component_path) - - def _updateIconSource(self): - if Application.getInstance().getTheme() is not None: - icon_name = self._getActiveOutputDeviceStatusIcon() - self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name)) - - ## Find the correct status icon depending on the active output device state - def _getActiveOutputDeviceStatusIcon(self): - # We assume that you are monitoring the device with the highest priority. - try: - output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] - except IndexError: - return "tab_status_unknown" - - if not output_device.acceptsCommands: - return "tab_status_unknown" - - if output_device.activePrinter is None: - return "tab_status_connected" - - # TODO: refactor to use enum instead of hardcoded strings? - if output_device.activePrinter.state == "maintenance": - return "tab_status_busy" - - if output_device.activePrinter.activePrintJob is None: - return "tab_status_connected" - - if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]: - return "tab_status_busy" - - if output_device.activePrinter.activePrintJob.state == "wait_cleanup": - return "tab_status_finished" - - if output_device.activePrinter.activePrintJob.state in ["ready", ""]: - return "tab_status_connected" - - if output_device.activePrinter.activePrintJob.state == "paused": - return "tab_status_paused" - - if output_device.activePrinter.activePrintJob.state == "error": - return "tab_status_stopped" - - return "tab_status_unknown" diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 35ce9cc37a..5c3dca9fae 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -158,9 +158,10 @@ 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 not None: + self.setActivity(False) + self.calculateMaxLayers() + self.calculateMaxPathsOnLayer(self._current_layer_num) def isBusy(self): return self._busy diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index bcd11b3cb7..c19c86d6ce 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject from time import time from datetime import datetime -from typing import Optional +from typing import Optional, Dict, List import json import os @@ -79,7 +79,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._latest_reply_handler = None - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) @@ -116,7 +115,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): @pyqtSlot() @pyqtSlot(str) - def sendPrintJob(self, target_printer = ""): + def sendPrintJob(self, target_printer: str = ""): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( @@ -157,11 +156,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return True @pyqtProperty(QObject, notify=activePrinterChanged) - def activePrinter(self) -> Optional["PrinterOutputModel"]: + def activePrinter(self) -> Optional[PrinterOutputModel]: return self._active_printer @pyqtSlot(QObject) - def setActivePrinter(self, printer): + def setActivePrinter(self, printer: Optional[PrinterOutputModel]): if self._active_printer != printer: if self._active_printer and self._active_printer.camera: self._active_printer.camera.stop() @@ -173,7 +172,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._compressing_gcode = False self._sending_gcode = False - def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): + def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get @@ -186,7 +185,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._progress_message.setProgress(0) self._progress_message.hide() - def _progressMessageActionTriggered(self, message_id=None, action_id=None): + def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None: if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() @@ -202,29 +201,29 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): @pyqtSlot() - def openPrintJobControlPanel(self): + def openPrintJobControlPanel(self) -> None: Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs")) @pyqtSlot() - def openPrinterControlPanel(self): + def openPrinterControlPanel(self) -> None: Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) @pyqtProperty("QVariantList", notify=printJobsChanged) - def printJobs(self): + def printJobs(self)-> List[PrintJobOutputModel] : return self._print_jobs @pyqtProperty("QVariantList", notify=printJobsChanged) - def queuedPrintJobs(self): + def queuedPrintJobs(self) -> List[PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None or print_job.state == "queued"] @pyqtProperty("QVariantList", notify=printJobsChanged) - def activePrintJobs(self): + def activePrintJobs(self) -> List[PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"] @pyqtProperty("QVariantList", notify=clusterPrintersChanged) - def connectedPrintersTypeCount(self): + def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]: printer_count = {} for printer in self._printers: if printer.type in printer_count: @@ -237,22 +236,22 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return result @pyqtSlot(int, result=str) - def formatDuration(self, seconds): + def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) @pyqtSlot(int, result=str) - def getTimeCompleted(self, time_remaining): + def getTimeCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute) @pyqtSlot(int, result=str) - def getDateCompleted(self, time_remaining): + def getDateCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() - def _printJobStateChanged(self): + def _printJobStateChanged(self) -> None: username = self._getUserName() if username is None: @@ -275,13 +274,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Keep a list of all completed jobs so we know if something changed next time. self._finished_jobs = finished_jobs - def _update(self): + def _update(self) -> None: if not super()._update(): return self.get("printers/", onFinished=self._onGetPrintersDataFinished) self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished) - def _onGetPrintJobsFinished(self, reply: QNetworkReply): + def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return @@ -323,7 +322,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if job_list_changed: self.printJobsChanged.emit() # Do a single emit for all print job changes. - def _onGetPrintersDataFinished(self, reply: QNetworkReply): + def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return @@ -352,34 +351,45 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if removed_printers or printer_list_changed: self.printersChanged.emit() - def _createPrinterModel(self, data): + def _createPrinterModel(self, data: Dict) -> PrinterOutputModel: printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) return printer - def _createPrintJobModel(self, data): + def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel: print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name= data["name"]) print_job.stateChanged.connect(self._printJobStateChanged) self._print_jobs.append(print_job) return print_job - def _updatePrintJob(self, print_job, data): + def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None: print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) print_job.updateState(data["status"]) print_job.updateOwner(data["owner"]) - def _updatePrinter(self, printer, data): + def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None: # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"] + definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"]) + if not definitions: + Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"]) + return + + machine_definition = definitions[0] + printer.updateName(data["friendly_name"]) printer.updateKey(data["uuid"]) printer.updateType(data["machine_variant"]) + + # Do not store the buildplate information that comes from connect if the current printer has not buildplate information + if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False): + printer.updateBuildplateName(data["build_plate"]["type"]) if not data["enabled"]: printer.updateState("disabled") else: @@ -416,7 +426,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): brand=brand, color=color, name=name) extruder.updateActiveMaterial(material) - def _removeJob(self, job): + def _removeJob(self, job: PrintJobOutputModel): if job not in self._print_jobs: return False @@ -427,7 +437,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return True - def _removePrinter(self, printer): + def _removePrinter(self, printer: PrinterOutputModel): self._printers.remove(printer) if self._active_printer == printer: self._active_printer = None diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index 0e872fed43..76e8721fdd 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -97,6 +97,25 @@ class DiscoverUM3Action(MachineAction): else: return [] + @pyqtSlot(str) + def setGroupName(self, group_name): + Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name) + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack: + meta_data = global_container_stack.getMetaData() + if "connect_group_name" in meta_data: + previous_connect_group_name = meta_data["connect_group_name"] + global_container_stack.setMetaDataEntry("connect_group_name", group_name) + # Find all the places where there is the same group name and change it accordingly + Application.getInstance().getMachineManager().replaceContainersMetadata(key = "connect_group_name", value = previous_connect_group_name, new_value = group_name) + else: + global_container_stack.addMetaDataEntry("connect_group_name", group_name) + global_container_stack.addMetaDataEntry("hidden", False) + + if self._network_plugin: + # Ensure that the connection states are refreshed. + self._network_plugin.reCheckConnections() + @pyqtSlot(str) def setKey(self, key): Logger.log("d", "Attempting to set the network key of the active machine to %s", key) @@ -104,11 +123,13 @@ class DiscoverUM3Action(MachineAction): if global_container_stack: meta_data = global_container_stack.getMetaData() if "um_network_key" in meta_data: + previous_network_key= meta_data["um_network_key"] global_container_stack.setMetaDataEntry("um_network_key", key) # Delete old authentication data. Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key) global_container_stack.removeMetaDataEntry("network_authentication_id") global_container_stack.removeMetaDataEntry("network_authentication_key") + Application.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key) else: global_container_stack.addMetaDataEntry("um_network_key", key) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index c0cb5a78b7..079e5dcdd3 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -32,10 +32,12 @@ Cura.MachineAction if(base.selectedDevice && base.completeProperties) { var printerKey = base.selectedDevice.key + var printerName = base.selectedDevice.name // TODO To change when the groups have a name if(manager.getStoredKey() != printerKey) { - manager.setKey(printerKey); - completed(); + manager.setKey(printerKey) + manager.setGroupName(printerName) // TODO To change when the groups have a name + completed() } } } @@ -303,7 +305,7 @@ Cura.MachineAction Button { text: catalog.i18nc("@action:button", "Connect") - enabled: (base.selectedDevice && base.completeProperties) ? true : false + enabled: (base.selectedDevice && base.completeProperties && base.selectedDevice.clusterSize > 0) ? true : false onClicked: connectToPrinter() } } diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py index 72f4f20262..50bb831ba8 100644 --- a/plugins/USBPrinting/AutoDetectBaudJob.py +++ b/plugins/USBPrinting/AutoDetectBaudJob.py @@ -22,6 +22,7 @@ class AutoDetectBaudJob(Job): def run(self): Logger.log("d", "Auto detect baud rate started.") timeout = 3 + tries = 2 programmer = Stk500v2() serial = None @@ -31,36 +32,38 @@ class AutoDetectBaudJob(Job): except: programmer.close() - for baud_rate in self._all_baud_rates: - Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate)) + for retry in range(tries): + for baud_rate in self._all_baud_rates: + Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate)) - if serial is None: - try: - serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout) - except SerialException as e: - Logger.logException("w", "Unable to create serial") - continue - else: - # We already have a serial connection, just change the baud rate. - try: - serial.baudrate = baud_rate - except: - continue - sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number - successful_responses = 0 - - serial.write(b"\n") # Ensure we clear out previous responses - serial.write(b"M105\n") - - timeout_time = time() + timeout - - while timeout_time > time(): - line = serial.readline() - if b"ok T:" in line: - successful_responses += 1 - if successful_responses >= 3: - self.setResult(baud_rate) - return + if serial is None: + try: + serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout) + except SerialException as e: + Logger.logException("w", "Unable to create serial") + continue + else: + # We already have a serial connection, just change the baud rate. + try: + serial.baudrate = baud_rate + except: + continue + sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number + successful_responses = 0 + serial.write(b"\n") # Ensure we clear out previous responses serial.write(b"M105\n") + + timeout_time = time() + timeout + + while timeout_time > time(): + line = serial.readline() + if b"ok T:" in line: + successful_responses += 1 + if successful_responses >= 3: + self.setResult(baud_rate) + return + + serial.write(b"M105\n") + sleep(15) # Give the printer some time to init and try again. self.setResult(None) # Unable to detect the correct baudrate. diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 11cc7bf472..14098b66f8 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -116,7 +116,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice): @pyqtSlot(str) def updateFirmware(self, file): - self._firmware_location = file + # the file path is qurl encoded. + self._firmware_location = file.replace("file://", "") self.showFirmwareInterface() self.setFirmwareUpdateState(FirmwareUpdateState.updating) self._update_firmware_thread.start() @@ -126,9 +127,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): if self._connection_state != ConnectionState.closed: self.close() - hex_file = intelHex.readHex(self._firmware_location) - if len(hex_file) == 0: - Logger.log("e", "Unable to read provided hex file. Could not update firmware") + try: + hex_file = intelHex.readHex(self._firmware_location) + assert len(hex_file) > 0 + except (FileNotFoundError, AssertionError): + Logger.log("e", "Unable to read provided hex file. Could not update firmware.") self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error) return @@ -198,7 +201,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # Reset line number. If this is not done, first line is sometimes ignored self._gcode.insert(0, "M110") self._gcode_position = 0 - self._is_printing = True self._print_start_time = time() self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds)) @@ -206,6 +208,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): for i in range(0, 4): # Push first 4 entries before accepting other inputs self._sendNextGcodeLine() + self._is_printing = True self.writeFinished.emit(self) def _autoDetectFinished(self, job: AutoDetectBaudJob): @@ -267,7 +270,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice): if not command.endswith(b"\n"): command += b"\n" try: - self._serial.write(b"\n") self._serial.write(command) except SerialTimeoutException: Logger.log("w", "Timeout when sending command to printer via USB.") @@ -284,7 +286,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self.sendCommand("M105") self._last_temperature_request = time() - if b"ok T:" in line or line.startswith(b"T:"): # Temperature message + if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) # Update all temperature values for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders): @@ -302,6 +304,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._printers[0].updateTargetBedTemperature(float(match[1])) if self._is_printing: + if line.startswith(b'!!'): + Logger.log('e', "Printer signals fatal error. Cancelling print. {}".format(line)) + self.cancelPrint() if b"ok" in line: if not self._command_queue.empty(): self._sendCommand(self._command_queue.get()) diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py b/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py index de2240a7c6..620f367e25 100644 --- a/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py +++ b/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py @@ -56,6 +56,8 @@ _EXTRUDER_TO_POSITION = { ## Upgrades configurations from the state they were in at version 3.2 to the # state they should be in at version 3.3. class VersionUpgrade32to33(VersionUpgrade): + + temporary_group_name_counter = 1 ## Gets the version number from a CFG file in Uranium's 3.2 format. # # Since the format may change, this is implemented for the 3.2 format only @@ -74,6 +76,28 @@ class VersionUpgrade32to33(VersionUpgrade): setting_version = int(parser.get("metadata", "setting_version", fallback = 0)) return format_version * 1000000 + setting_version + ## Upgrades a container stack from version 3.2 to 3.3. + # + # \param serialised The serialised form of a container stack. + # \param filename The name of the file to upgrade. + def upgradeStack(self, serialized, filename): + parser = configparser.ConfigParser(interpolation = None) + parser.read_string(serialized) + + if "metadata" in parser and "um_network_key" in parser["metadata"]: + if "hidden" not in parser["metadata"]: + parser["metadata"]["hidden"] = "False" + if "connect_group_name" not in parser["metadata"]: + parser["metadata"]["connect_group_name"] = "Temporary group name #" + str(self.temporary_group_name_counter) + self.temporary_group_name_counter += 1 + + #Update version number. + parser["general"]["version"] = "4" + + result = io.StringIO() + parser.write(result) + return [filename], [result.getvalue()] + ## Upgrades non-quality-changes instance containers to have the new version # number. def upgradeInstanceContainer(self, serialized, filename): diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py b/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py index c411b4190e..72ff6e1de9 100644 --- a/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py +++ b/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py @@ -9,11 +9,22 @@ def getMetaData(): return { "version_upgrade": { # From To Upgrade function + ("machine_stack", 3000004): ("machine_stack", 4000004, upgrade.upgradeStack), + ("extruder_train", 3000004): ("extruder_train", 4000004, upgrade.upgradeStack), + ("definition_changes", 2000004): ("definition_changes", 3000004, upgrade.upgradeInstanceContainer), ("quality_changes", 2000004): ("quality_changes", 3000004, upgrade.upgradeQualityChanges), ("user", 2000004): ("user", 3000004, upgrade.upgradeInstanceContainer) }, "sources": { + "machine_stack": { + "get_version": upgrade.getCfgVersion, + "location": {"./machine_instances"} + }, + "extruder_train": { + "get_version": upgrade.getCfgVersion, + "location": {"./extruders"} + }, "definition_changes": { "get_version": upgrade.getCfgVersion, "location": {"./definition_changes"} diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index bbf5dfe2ba..8b17721794 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -277,7 +277,7 @@ class XmlMaterialProfile(InstanceContainer): # Compatible is a special case, as it's added as a meta data entry (instead of an instance). material_container = variant_dict["material_container"] - compatible = container.getMetaDataEntry("compatible") + compatible = material_container.getMetaDataEntry("compatible") if compatible is not None: builder.start("setting", {"key": "hardware compatible"}) if compatible: @@ -838,15 +838,11 @@ class XmlMaterialProfile(InstanceContainer): if machine_compatibility: new_material_id = container_id + "_" + machine_id - # The child or derived material container may already exist. This can happen when a material in a - # project file and the a material in Cura have the same ID. - # In the case if a derived material already exists, override that material container because if - # the data in the parent material has been changed, the derived ones should be updated too. - found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_material_id) - if found_materials: - new_material_metadata = found_materials[0] - else: - new_material_metadata = {} + # Do not look for existing container/container metadata with the same ID although they may exist. + # In project loading and perhaps some other places, we only want to get information (metadata) + # from a file without changing the current state of the system. If we overwrite the existing + # metadata here, deserializeMetadata() will not be safe for retrieving information. + new_material_metadata = {} new_material_metadata.update(base_metadata) new_material_metadata["id"] = new_material_id @@ -854,8 +850,7 @@ class XmlMaterialProfile(InstanceContainer): new_material_metadata["machine_manufacturer"] = machine_manufacturer new_material_metadata["definition"] = machine_id - if len(found_materials) == 0: #This is a new material. - result_metadata.append(new_material_metadata) + result_metadata.append(new_material_metadata) buildplates = machine.iterfind("./um:buildplate", cls.__namespaces) buildplate_map = {} @@ -866,12 +861,12 @@ class XmlMaterialProfile(InstanceContainer): if buildplate_id is None: continue - variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = buildplate_id) - if not variant_containers: + variant_metadata = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = buildplate_id) + if not variant_metadata: # It is not really properly defined what "ID" is so also search for variants by name. - variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = buildplate_id) + variant_metadata = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = buildplate_id) - if not variant_containers: + if not variant_metadata: continue settings = buildplate.iterfind("./um:setting", cls.__namespaces) @@ -900,12 +895,8 @@ class XmlMaterialProfile(InstanceContainer): new_hotend_specific_material_id = container_id + "_" + machine_id + "_" + hotend_name.replace(" ", "_") - # Same as machine compatibility, keep the derived material containers consistent with the parent material - found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_hotend_specific_material_id) - if found_materials: - new_hotend_material_metadata = found_materials[0] - else: - new_hotend_material_metadata = {} + # Same as above, do not overwrite existing metadata. + new_hotend_material_metadata = {} new_hotend_material_metadata.update(base_metadata) new_hotend_material_metadata["variant_name"] = hotend_name @@ -917,8 +908,7 @@ class XmlMaterialProfile(InstanceContainer): new_hotend_material_metadata["buildplate_compatible"] = buildplate_map["buildplate_compatible"] new_hotend_material_metadata["buildplate_recommended"] = buildplate_map["buildplate_recommended"] - if len(found_materials) == 0: - result_metadata.append(new_hotend_material_metadata) + result_metadata.append(new_hotend_material_metadata) # there is only one ID for a machine. Once we have reached here, it means we have already found # a workable ID for that machine, so there is no need to continue diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 6d4688bb52..21ee543333 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -215,7 +215,7 @@ { "label": "Number of Extruders that are enabled", "description": "Number of extruder trains that are enabled; automatically set in software", - "default_value": "machine_extruder_count", + "value": "machine_extruder_count", "minimum_value": "1", "maximum_value": "16", "type": "int", @@ -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/printrbot_simple_makers_kit.def.json b/resources/definitions/printrbot_simple_makers_kit.def.json new file mode 100644 index 0000000000..e2afd57826 --- /dev/null +++ b/resources/definitions/printrbot_simple_makers_kit.def.json @@ -0,0 +1,38 @@ +{ + "version": 2, + "name": "Printrbot Simple Maker's Kit (1405)", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Timur Tabi", + "manufacturer": "Printrbot", + "file_formats": "text/x-gcode" + }, + + "overrides": { + "machine_name": { "default_value": "Printrbot Simple Maker's Kit (1405)" }, + "machine_heated_bed": { "default_value": false }, + "machine_width": { "default_value": 100 }, + "machine_depth": { "default_value": 100 }, + "machine_height": { "default_value": 115 }, + "material_diameter": { "default_value": 1.75 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "machine_head_with_fans_polygon": { + "default_value": [ + [-40, 1000], + [-40, -10], + [60, 1000], + [60, -10] + ] + }, + "gantry_height": { "default_value": 1000 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + + "machine_start_gcode": { + "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;home X/Y\nG28 Z0 ;home Z\nG92 E0 ;zero the extruded length\nG29 ;initiate auto bed leveling sequence" + }, + "machine_end_gcode": { + "default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nM106 S0 ;fan off\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit\nG1 Z+1 E-5 F9000 ;move Z up a bit and retract even more\nG28 X0 Y0 ;home X/Y, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning" + } + } +} diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 4e2b5ad4c2..57cfbe960f 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -126,7 +126,7 @@ "retraction_count_max": { "value": "10" }, "retraction_extrusion_window": { "value": "1" }, "retraction_hop": { "value": "2" }, - "retraction_hop_enabled": { "value": "True" }, + "retraction_hop_enabled": { "value": "extruders_enabled_count > 1" }, "retraction_hop_only_when_collides": { "value": "True" }, "retraction_min_travel": { "value": "5" }, "retraction_prime_speed": { "value": "15" }, @@ -150,7 +150,7 @@ "switch_extruder_prime_speed": { "value": "15" }, "switch_extruder_retraction_amount": { "value": "8" }, "top_bottom_thickness": { "value": "1" }, - "travel_avoid_distance": { "value": "3" }, + "travel_avoid_distance": { "value": "3 if extruders_enabled_count > 1 else machine_nozzle_tip_outer_diameter / 2 * 1.5" }, "wall_0_inset": { "value": "0" }, "wall_line_width_x": { "value": "round(wall_line_width * 0.3 / 0.35, 2)" }, "wall_thickness": { "value": "1" }, diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index c81281f0d7..cff4399073 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -653,7 +653,10 @@ UM.MainWindow { preferences.visible = true; preferences.setPage(1); - preferences.getCurrentItem().scrollToSection(source.key); + if(source && source.key) + { + preferences.getCurrentItem().scrollToSection(source.key); + } } } diff --git a/resources/qml/MachineSelection.qml b/resources/qml/MachineSelection.qml index e40731f3ca..b959e20bb7 100644 --- a/resources/qml/MachineSelection.qml +++ b/resources/qml/MachineSelection.qml @@ -12,7 +12,11 @@ import "Menus" ToolButton { - text: Cura.MachineManager.activeMachineName + 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 @@ -22,16 +26,13 @@ ToolButton { color: { - if(control.pressed) - { + if (control.pressed) { return UM.Theme.getColor("sidebar_header_active"); } - else if(control.hovered) - { + else if (control.hovered) { return UM.Theme.getColor("sidebar_header_hover"); } - else - { + else { return UM.Theme.getColor("sidebar_header_bar"); } } @@ -50,18 +51,32 @@ ToolButton color: UM.Theme.getColor("text_emphasis") source: UM.Theme.getIcon("arrow_bottom") } + + PrinterStatusIcon + { + id: printerStatusIcon + visible: printerConnected || isNetworkPrinter + status: printerStatus + anchors + { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: UM.Theme.getSize("sidebar_margin").width + } + } + Label { id: sidebarComboBoxLabel color: UM.Theme.getColor("sidebar_header_text_active") text: control.text; elide: Text.ElideRight; - anchors.left: parent.left; - anchors.leftMargin: UM.Theme.getSize("default_margin").width * 2 + anchors.left: isNetworkPrinter ? printerStatusIcon.right : parent.left; + anchors.leftMargin: isNetworkPrinter ? UM.Theme.getSize("sidebar_lining").width : UM.Theme.getSize("sidebar_margin").width anchors.right: downArrow.left; anchors.rightMargin: control.rightMargin; anchors.verticalCenter: parent.verticalCenter; - font: UM.Theme.getFont("large") + font: UM.Theme.getFont("medium_bold") } } label: Label {} diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml new file mode 100644 index 0000000000..be8c8bcb45 --- /dev/null +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml @@ -0,0 +1,124 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.0 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Rectangle +{ + id: configurationItem + + property var configuration: null + property var selected: false + signal activateConfiguration() + + height: childrenRect.height + border.width: UM.Theme.getSize("default_lining").width + border.color: updateBorderColor() + color: selected ? UM.Theme.getColor("configuration_item_active") : UM.Theme.getColor("configuration_item") + property var textColor: selected ? UM.Theme.getColor("configuration_item_text_active") : UM.Theme.getColor("configuration_item_text") + + function updateBorderColor() + { + border.color = selected ? UM.Theme.getColor("configuration_item_border_active") : UM.Theme.getColor("configuration_item_border") + } + + Column + { + id: contentColumn + width: parent.width + padding: UM.Theme.getSize("default_margin").width + spacing: Math.round(UM.Theme.getSize("default_margin").height / 2) + + Row + { + id: extruderRow + + width: parent.width - 2 * parent.padding + height: childrenRect.height + + spacing: UM.Theme.getSize("default_margin").width + + Repeater + { + id: repeater + height: childrenRect.height + model: configuration.extruderConfigurations + delegate: PrintCoreConfiguration + { + width: Math.round(parent.width / 2) + printCoreConfiguration: modelData + mainColor: textColor + } + } + } + + //Buildplate row separator + Rectangle + { + id: separator + + visible: buildplateInformation.visible + width: parent.width - 2 * parent.padding + height: visible ? Math.round(UM.Theme.getSize("sidebar_lining_thin").height / 2) : 0 + color: textColor + } + + Item + { + id: buildplateInformation + width: parent.width - 2 * parent.padding + height: childrenRect.height + visible: configuration.buildplateConfiguration != "" + + UM.RecolorImage { + id: buildplateIcon + anchors.left: parent.left + width: UM.Theme.getSize("topbar_button_icon").width + height: UM.Theme.getSize("topbar_button_icon").height + sourceSize.width: width + sourceSize.height: height + source: UM.Theme.getIcon("buildplate") + color: textColor + } + + Label + { + id: buildplateLabel + anchors.left: buildplateIcon.right + anchors.verticalCenter: buildplateIcon.verticalCenter + anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").height / 2) + text: configuration.buildplateConfiguration + color: textColor + } + } + } + + MouseArea + { + id: mouse + anchors.fill: parent + onClicked: activateConfiguration() + hoverEnabled: true + onEntered: parent.border.color = UM.Theme.getColor("configuration_item_border_hover") + onExited: updateBorderColor() + } + + Connections + { + target: Cura.MachineManager + onCurrentConfigurationChanged: { + configurationItem.selected = Cura.MachineManager.matchesConfiguration(configuration) + updateBorderColor() + } + } + + Component.onCompleted: + { + configurationItem.selected = Cura.MachineManager.matchesConfiguration(configuration) + updateBorderColor() + } +} \ No newline at end of file diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml new file mode 100644 index 0000000000..999fecd7fd --- /dev/null +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml @@ -0,0 +1,86 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Column +{ + id: base + property var outputDevice: null + property var computedHeight: container.height + configurationListHeading.height + 3 * padding + height: childrenRect.height + 2 * padding + padding: UM.Theme.getSize("default_margin").width + spacing: Math.round(UM.Theme.getSize("default_margin").height / 2) + + Label + { + id: configurationListHeading + text: catalog.i18nc("@label:header configurations", "Available configurations") + font: UM.Theme.getFont("large") + width: parent.width - 2 * parent.padding + } + + Component + { + id: sectionHeading + Rectangle + { + height: childrenRect.height + UM.Theme.getSize("default_margin").height + Label + { + text: section + font: UM.Theme.getFont("default_bold") + } + } + } + + ScrollView + { + id: container + width: parent.width - parent.padding + height: Math.min(configurationList.contentHeight, 350 * screenScaleFactor) + + style: UM.Theme.styles.scrollview + __wheelAreaScrollSpeed: 75 // Scroll three lines in one scroll event + + ListView + { + id: configurationList + spacing: Math.round(UM.Theme.getSize("default_margin").height / 2) + width: container.width + contentHeight: childrenRect.height + + section.property: "modelData.printerType" + section.criteria: ViewSection.FullString + section.delegate: sectionHeading + + model: (outputDevice != null) ? outputDevice.uniqueConfigurations : [] + delegate: ConfigurationItem + { + width: parent.width - UM.Theme.getSize("default_margin").width + configuration: modelData + onActivateConfiguration: + { + switchPopupState() + Cura.MachineManager.applyRemoteConfiguration(configuration) + } + } + } + } + + Connections + { + target: outputDevice + onUniqueConfigurationsChanged: + { + // FIXME For now the model should be removed and then created again, otherwise changes in the printer don't automatically update the UI + configurationList.model = [] + configurationList.model = outputDevice.uniqueConfigurations + } + } +} \ No newline at end of file diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml new file mode 100644 index 0000000000..d7ee2c68ee --- /dev/null +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml @@ -0,0 +1,65 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Controls.Styles 1.4 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Item +{ + id: configurationSelector + property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null + property var panelWidth: control.width + + function switchPopupState() + { + popup.visible ? popup.close() : popup.open() + } + + SyncButton + { + id: syncButton + onClicked: switchPopupState() + outputDevice: connectedDevice + } + + 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: false + padding: UM.Theme.getSize("default_lining").width + transformOrigin: Popup.Top + contentItem: ConfigurationListView + { + id: configList + width: panelWidth - 2 * popup.padding + outputDevice: connectedDevice + } + background: Rectangle + { + color: UM.Theme.getColor("setting_control") + border.color: UM.Theme.getColor("setting_control_border") + } + exit: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + 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/PrintCoreConfiguration.qml b/resources/qml/Menus/ConfigurationMenu/PrintCoreConfiguration.qml new file mode 100644 index 0000000000..ca1b666e69 --- /dev/null +++ b/resources/qml/Menus/ConfigurationMenu/PrintCoreConfiguration.qml @@ -0,0 +1,87 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.0 + +import UM 1.2 as UM + + +Column +{ + id: extruderInfo + property var printCoreConfiguration + property var mainColor: "black" + spacing: Math.round(UM.Theme.getSize("default_margin").height / 2) + + height: childrenRect.height + + Item + { + id: extruder + width: parent.width + height: childrenRect.height + + Label + { + id: extruderLabel + text: catalog.i18nc("@label:extruder label", "Extruder") + elide: Text.ElideRight + anchors.left: parent.left + font: UM.Theme.getFont("default") + color: mainColor + } + + // Rounded item to show the extruder number + Item + { + id: extruderIconItem + anchors.verticalCenter: extruderLabel.verticalCenter + anchors.left: extruderLabel.right + anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width / 2) + + width: UM.Theme.getSize("section_icon").width + height: UM.Theme.getSize("section_icon").height + + UM.RecolorImage { + id: mainCircle + anchors.fill: parent + + anchors.centerIn: parent + sourceSize.width: parent.width + sourceSize.height: parent.height + source: UM.Theme.getIcon("extruder_button") + color: mainColor + } + + Label + { + id: extruderNumberText + anchors.centerIn: parent + text: printCoreConfiguration.position + 1 + font: UM.Theme.getFont("default") + color: mainColor + } + } + } + + Label + { + id: materialLabel + text: printCoreConfiguration.material.name + elide: Text.ElideRight + width: parent.width + font: UM.Theme.getFont("default_bold") + color: mainColor + } + + Label + { + id: printCoreTypeLabel + text: printCoreConfiguration.hotendID + elide: Text.ElideRight + width: parent.width + font: UM.Theme.getFont("default") + color: mainColor + } +} diff --git a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml new file mode 100644 index 0000000000..c292a792db --- /dev/null +++ b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml @@ -0,0 +1,102 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Button +{ + id: base + property var outputDevice: null + property var matched: updateOnSync() + text: matched == true ? "Yes" : "No" + width: parent.width + height: parent.height + + function updateOnSync() + { + if (outputDevice != undefined) + { + for (var index in outputDevice.uniqueConfigurations) + { + var configuration = outputDevice.uniqueConfigurations[index] + if (Cura.MachineManager.matchesConfiguration(configuration)) + { + base.matched = true; + return; + } + } + } + base.matched = false; + } + + style: ButtonStyle + { + background: Rectangle + { + color: + { + if(control.pressed) + { + return UM.Theme.getColor("sidebar_header_active"); + } + else if(control.hovered) + { + return UM.Theme.getColor("sidebar_header_hover"); + } + else + { + return UM.Theme.getColor("sidebar_header_bar"); + } + } + Behavior on color { ColorAnimation { duration: 50; } } + + UM.RecolorImage + { + id: downArrow + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + width: UM.Theme.getSize("standard_arrow").width + height: UM.Theme.getSize("standard_arrow").height + sourceSize.width: width + sourceSize.height: height + color: UM.Theme.getColor("text_emphasis") + source: UM.Theme.getIcon("arrow_bottom") + } + UM.RecolorImage { + id: sidebarComboBoxLabel + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: parent.verticalCenter; + + width: UM.Theme.getSize("printer_sync_icon").width + height: UM.Theme.getSize("printer_sync_icon").height + + color: control.matched ? UM.Theme.getColor("printer_config_matched") : UM.Theme.getColor("printer_config_mismatch") + source: UM.Theme.getIcon("tab_status_connected") + sourceSize.width: width + sourceSize.height: height + } + } + label: Label {} + } + + Connections { + target: outputDevice + onUniqueConfigurationsChanged: { + updateOnSync() + } + } + + Connections { + target: Cura.MachineManager + onCurrentConfigurationChanged: { + updateOnSync() + } + } +} \ No newline at end of file diff --git a/resources/qml/Menus/LocalPrinterMenu.qml b/resources/qml/Menus/LocalPrinterMenu.qml new file mode 100644 index 0000000000..0bdd4f33b9 --- /dev/null +++ b/resources/qml/Menus/LocalPrinterMenu.qml @@ -0,0 +1,23 @@ +// 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.4 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Instantiator { + model: UM.ContainerStacksModel { + filter: {"type": "machine", "um_network_key": null} + } + MenuItem { + text: model.name; + checkable: true; + checked: Cura.MachineManager.activeMachineId == model.id + exclusiveGroup: group; + onTriggered: Cura.MachineManager.setActiveMachine(model.id); + } + onObjectAdded: menu.insertItem(index, object) + onObjectRemoved: menu.removeItem(object) +} diff --git a/resources/qml/Menus/NetworkPrinterMenu.qml b/resources/qml/Menus/NetworkPrinterMenu.qml new file mode 100644 index 0000000000..07a22202e4 --- /dev/null +++ b/resources/qml/Menus/NetworkPrinterMenu.qml @@ -0,0 +1,25 @@ +// 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.4 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Instantiator { + model: UM.ContainerStacksModel { + filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} + } + MenuItem { + // TODO: Use printer_group icon when it's a cluster. Not use it for now since it doesn't look as expected +// iconSource: UM.Theme.getIcon("printer_single") + text: model.metadata["connect_group_name"] + checkable: true; + checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] + exclusiveGroup: group; + onTriggered: Cura.MachineManager.setActiveMachine(model.id); + } + onObjectAdded: menu.insertItem(index, object) + onObjectRemoved: menu.removeItem(object) +} diff --git a/resources/qml/Menus/PrinterMenu.qml b/resources/qml/Menus/PrinterMenu.qml index 073723a60d..741d927c13 100644 --- a/resources/qml/Menus/PrinterMenu.qml +++ b/resources/qml/Menus/PrinterMenu.qml @@ -1,37 +1,60 @@ -// Copyright (c) 2016 Ultimaker B.V. +// Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 -import QtQuick.Controls 1.1 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import UM 1.2 as UM import Cura 1.0 as Cura Menu { - id: menu; + id: menu +// TODO Enable custom style to the menu +// style: MenuStyle +// { +// frame: Rectangle +// { +// color: "white" +// } +// } - Instantiator + MenuItem { - model: UM.ContainerStacksModel - { - filter: {"type": "machine"} - } - MenuItem - { - text: model.name; - checkable: true; - checked: Cura.MachineManager.activeMachineId == model.id - exclusiveGroup: group; - onTriggered: Cura.MachineManager.setActiveMachine(model.id); - } - onObjectAdded: menu.insertItem(index, object) - onObjectRemoved: menu.removeItem(object) + text: catalog.i18nc("@label:category menu label", "Network enabled printers") + enabled: false + visible: networkPrinterMenu.count > 0 + } + + NetworkPrinterMenu + { + id: networkPrinterMenu + } + + MenuSeparator + { + visible: networkPrinterMenu.count > 0 + } + + MenuItem + { + text: catalog.i18nc("@label:category menu label", "Local printers") + enabled: false + visible: localPrinterMenu.count > 0 + } + + LocalPrinterMenu + { + id: localPrinterMenu } ExclusiveGroup { id: group; } - MenuSeparator { } + MenuSeparator + { + visible: localPrinterMenu.count > 0 + } MenuItem { action: Cura.Actions.addMachine; } MenuItem { action: Cura.Actions.configureMachines; } diff --git a/resources/qml/Menus/PrinterStatusIcon.qml b/resources/qml/Menus/PrinterStatusIcon.qml new file mode 100644 index 0000000000..6ff6b07af8 --- /dev/null +++ b/resources/qml/Menus/PrinterStatusIcon.qml @@ -0,0 +1,27 @@ +// Copyright (c) 2017 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Item +{ + property var status: "disconnected" + width: childrenRect.width + height: childrenRect.height + UM.RecolorImage + { + id: statusIcon + width: UM.Theme.getSize("printer_status_icon").width + height: UM.Theme.getSize("printer_status_icon").height + sourceSize.width: width + sourceSize.height: width + color: UM.Theme.getColor("tab_status_" + parent.status) + source: UM.Theme.getIcon(parent.status) + } +} + + + diff --git a/resources/qml/Menus/PrinterTypeMenu.qml b/resources/qml/Menus/PrinterTypeMenu.qml new file mode 100644 index 0000000000..28bdca54d9 --- /dev/null +++ b/resources/qml/Menus/PrinterTypeMenu.qml @@ -0,0 +1,37 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 1.4 + +import UM 1.3 as UM +import Cura 1.0 as Cura + +Menu +{ + id: menu + title: "Printer type" + property var outputDevice: Cura.MachineManager.printerOutputDevices[0] + + Instantiator + { + id: printerTypeInstantiator + model: outputDevice != null ? outputDevice.connectedPrintersTypeCount : [] + + MenuItem + { + text: modelData.machine_type + checkable: true + checked: Cura.MachineManager.activeMachineDefinitionName == modelData.machine_type + exclusiveGroup: group + onTriggered: + { + Cura.MachineManager.switchPrinterType(modelData.machine_type) + } + } + onObjectAdded: menu.insertItem(index, object) + onObjectRemoved: menu.removeItem(object) + } + + ExclusiveGroup { id: group } +} diff --git a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml new file mode 100644 index 0000000000..19c36e6118 --- /dev/null +++ b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml @@ -0,0 +1,82 @@ +// 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 UM 1.2 as UM +import Cura 1.0 as Cura + +Menu +{ + id: menu + title: catalog.i18nc("@action:inmenu", "Visible Settings") + + 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 + + MenuItem + { + text: model.name + checkable: true + checked: model.id == Cura.SettingVisibilityPresetsModel.activePreset + exclusiveGroup: group + onTriggered: + { + Cura.SettingVisibilityPresetsModel.setActivePreset(model.id); + + UM.Preferences.setValue("general/visible_settings", model.settings.join(";")); + + showSettingVisibilityProfile(); + } + } + + onObjectAdded: menu.insertItem(index, object) + onObjectRemoved: menu.removeItem(object) + } + + MenuSeparator {} + MenuItem + { + text: catalog.i18nc("@action:inmenu", "All Settings") + checkable: true + checked: showingAllSettings + exclusiveGroup: group + onTriggered: + { + showAllSettings(); + } + } + MenuSeparator {} + MenuItem + { + text: catalog.i18nc("@action:inmenu", "Manage Setting Visibility...") + iconName: "configure" + onTriggered: Cura.Actions.configureSettingVisibility.trigger() + } + + ExclusiveGroup { id: group } +} diff --git a/resources/qml/Preferences/MachinesPage.qml b/resources/qml/Preferences/MachinesPage.qml index 62e5ef98b4..665586d29f 100644 --- a/resources/qml/Preferences/MachinesPage.qml +++ b/resources/qml/Preferences/MachinesPage.qml @@ -14,10 +14,7 @@ UM.ManagementPage id: base; title: catalog.i18nc("@title:tab", "Printers"); - model: UM.ContainerStacksModel - { - filter: {"type": "machine"} - } + model: Cura.MachineManagementModel { } activeId: Cura.MachineManager.activeMachineId activeIndex: activeMachineIndex() @@ -57,7 +54,7 @@ UM.ManagementPage { text: catalog.i18nc("@action:button", "Rename"); iconName: "edit-rename"; - enabled: base.currentItem != null + enabled: base.currentItem != null && base.currentItem.metadata.connect_group_name == null onClicked: renameDialog.open(); } ] diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 553cfe0423..7f06ffecde 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -19,14 +19,17 @@ Item UM.I18nCatalog { id: catalog; name: "cura"; } - Cura.MaterialManagementModel { + Cura.MaterialManagementModel + { id: materialsModel } - Label { + Label + { id: titleLabel - anchors { + anchors + { top: parent.top left: parent.left right: parent.right @@ -170,22 +173,27 @@ Item Connections { target: materialsModel - onItemsChanged: { + onItemsChanged: + { var currentItemId = base.currentItem == null ? "" : base.currentItem.root_material_id; var position = Cura.ExtruderManager.activeExtruderIndex; // try to pick the currently selected item; it may have been moved - if (base.newRootMaterialIdToSwitchTo == "") { + if (base.newRootMaterialIdToSwitchTo == "") + { base.newRootMaterialIdToSwitchTo = currentItemId; } - for (var idx = 0; idx < materialsModel.rowCount(); ++idx) { + for (var idx = 0; idx < materialsModel.rowCount(); ++idx) + { var item = materialsModel.getItem(idx); - if (item.root_material_id == base.newRootMaterialIdToSwitchTo) { + if (item.root_material_id == base.newRootMaterialIdToSwitchTo) + { // Switch to the newly created profile if needed materialListView.currentIndex = idx; materialListView.activateDetailsWithIndex(materialListView.currentIndex); - if (base.toActivateNewMaterial) { + if (base.toActivateNewMaterial) + { Cura.MachineManager.setMaterial(position, item.container_node); } base.newRootMaterialIdToSwitchTo = ""; @@ -196,7 +204,8 @@ Item materialListView.currentIndex = 0; materialListView.activateDetailsWithIndex(materialListView.currentIndex); - if (base.toActivateNewMaterial) { + if (base.toActivateNewMaterial) + { Cura.MachineManager.setMaterial(position, materialsModel.getItem(0).container_node); } base.newRootMaterialIdToSwitchTo = ""; @@ -233,14 +242,17 @@ Item messageDialog.title = catalog.i18nc("@title:window", "Import Material"); messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tags or !", "Could not import material %1: %2").arg(fileUrl).arg(result.message); - if (result.status == "success") { + if (result.status == "success") + { messageDialog.icon = StandardIcon.Information; messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tag !", "Successfully imported material %1").arg(fileUrl); } - else if (result.status == "duplicate") { + else if (result.status == "duplicate") + { messageDialog.icon = StandardIcon.Warning; } - else { + else + { messageDialog.icon = StandardIcon.Critical; } messageDialog.open(); @@ -260,12 +272,14 @@ Item var result = Cura.ContainerManager.exportContainer(base.currentItem.root_material_id, selectedNameFilter, fileUrl); messageDialog.title = catalog.i18nc("@title:window", "Export Material"); - if (result.status == "error") { + if (result.status == "error") + { messageDialog.icon = StandardIcon.Critical; messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tags and !", "Failed to export material to %1: %2").arg(fileUrl).arg(result.message); messageDialog.open(); } - else if (result.status == "success") { + else if (result.status == "success") + { messageDialog.icon = StandardIcon.Information; messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tag !", "Successfully exported material to %1").arg(result.path); messageDialog.open(); @@ -283,7 +297,8 @@ Item Item { id: contentsItem - anchors { + anchors + { top: titleLabel.bottom left: parent.left right: parent.right @@ -297,7 +312,8 @@ Item Item { - anchors { + anchors + { top: buttonRow.bottom topMargin: UM.Theme.getSize("default_margin").height left: parent.left @@ -310,12 +326,14 @@ Item Label { id: captionLabel - anchors { + anchors + { top: parent.top left: parent.left } visible: text != "" - text: { + text: + { var caption = catalog.i18nc("@action:label", "Printer") + ": " + Cura.MachineManager.activeMachineName; if (Cura.MachineManager.hasVariants) { @@ -330,14 +348,16 @@ Item ScrollView { id: materialScrollView - anchors { + anchors + { top: captionLabel.visible ? captionLabel.bottom : parent.top topMargin: captionLabel.visible ? UM.Theme.getSize("default_margin").height : 0 bottom: parent.bottom left: parent.left } - Rectangle { + Rectangle + { parent: viewport anchors.fill: parent color: palette.light @@ -418,13 +438,15 @@ Item MouseArea { anchors.fill: parent - onClicked: { + onClicked: + { parent.ListView.view.currentIndex = model.index; } } } - function activateDetailsWithIndex(index) { + function activateDetailsWithIndex(index) + { var model = materialsModel.getItem(index); base.currentItem = model; materialDetailsView.containerId = model.container_id; @@ -446,7 +468,8 @@ Item { id: detailsPanel - anchors { + anchors + { left: materialScrollView.right leftMargin: UM.Theme.getSize("default_margin").width top: parent.top diff --git a/resources/qml/Preferences/SettingVisibilityPage.qml b/resources/qml/Preferences/SettingVisibilityPage.qml index 0e3069d194..f0c24e2cbe 100644 --- a/resources/qml/Preferences/SettingVisibilityPage.qml +++ b/resources/qml/Preferences/SettingVisibilityPage.qml @@ -26,8 +26,8 @@ UM.PreferencesPage UM.Preferences.resetPreference("general/visible_settings") // After calling this function update Setting visibility preset combobox. - // Reset should set "Basic" setting preset - visibilityPreset.setBasicPreset() + // Reset should set default setting preset ("Basic") + visibilityPreset.setDefaultPreset() } resetEnabled: true; @@ -37,6 +37,8 @@ UM.PreferencesPage id: base; anchors.fill: parent; + property bool inhibitSwitchToCustom: false + CheckBox { id: toggleVisibleSettings @@ -84,7 +86,7 @@ UM.PreferencesPage if (visibilityPreset.currentIndex != visibilityPreset.model.count - 1) { visibilityPreset.currentIndex = visibilityPreset.model.count - 1 - UM.Preferences.setValue("general/preset_setting_visibility_choice", visibilityPreset.model.get(visibilityPreset.currentIndex).text) + UM.Preferences.setValue("cura/active_setting_visibility_preset", visibilityPreset.model.getItem(visibilityPreset.currentIndex).id) } } } @@ -110,25 +112,13 @@ UM.PreferencesPage ComboBox { - property int customOptionValue: 100 - - function setBasicPreset() + function setDefaultPreset() { - var index = 0 - for(var i = 0; i < presetNamesList.count; ++i) - { - if(model.get(i).text == "Basic") - { - index = i; - break; - } - } - - visibilityPreset.currentIndex = index + visibilityPreset.currentIndex = 0 } id: visibilityPreset - width: 150 + width: 150 * screenScaleFactor anchors { top: parent.top @@ -137,56 +127,49 @@ UM.PreferencesPage model: ListModel { - id: presetNamesList + id: visibilityPresetsModel Component.onCompleted: { - // returned value is Dictionary (Ex: {1:"Basic"}, The number 1 is the weight and sort by weight) - var itemsDict = UM.Preferences.getValue("general/visible_settings_preset") - var sorted = []; - for(var key in itemsDict) { - sorted[sorted.length] = key; - } + visibilityPresetsModel.append({text: catalog.i18nc("@action:inmenu", "Custom selection"), id: "custom"}); - sorted.sort(); - for(var i = 0; i < sorted.length; i++) { - presetNamesList.append({text: itemsDict[sorted[i]], value: i}); + var presets = Cura.SettingVisibilityPresetsModel; + for(var i = 0; i < presets.rowCount(); i++) + { + visibilityPresetsModel.append({text: presets.getItem(i)["name"], id: presets.getItem(i)["id"]}); } - - // By agreement lets "Custom" option will have value 100 - presetNamesList.append({text: "Custom", value: visibilityPreset.customOptionValue}); } } currentIndex: { // Load previously selected preset. - var text = UM.Preferences.getValue("general/preset_setting_visibility_choice"); - - - - var index = 0; - for(var i = 0; i < presetNamesList.count; ++i) + var index = Cura.SettingVisibilityPresetsModel.find("id", Cura.SettingVisibilityPresetsModel.activePreset); + if(index == -1) { - if(model.get(i).text == text) - { - index = i; - break; - } + return 0; } - return index; + + return index + 1; // "Custom selection" entry is added in front, so index is off by 1 } onActivated: { - // TODO What to do if user is selected "Custom from Combobox" ? - if (model.get(index).text == "Custom"){ - UM.Preferences.setValue("general/preset_setting_visibility_choice", model.get(index).text) - return - } + base.inhibitSwitchToCustom = true; + var preset_id = visibilityPresetsModel.get(index).id; + Cura.SettingVisibilityPresetsModel.setActivePreset(preset_id); - var newVisibleSettings = CuraApplication.getVisibilitySettingPreset(model.get(index).text) - UM.Preferences.setValue("general/visible_settings", newVisibleSettings) - UM.Preferences.setValue("general/preset_setting_visibility_choice", model.get(index).text) + 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; } } @@ -216,7 +199,16 @@ UM.PreferencesPage exclude: ["machine_settings", "command_line_settings"] showAncestors: true expanded: ["*"] - visibilityHandler: UM.SettingPreferenceVisibilityHandler { } + visibilityHandler: UM.SettingPreferenceVisibilityHandler + { + onVisibilityChanged: + { + if(Cura.SettingVisibilityPresetsModel.activePreset != "" && !base.inhibitSwitchToCustom) + { + Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + } + } + } } delegate: Loader @@ -259,19 +251,7 @@ UM.PreferencesPage { id: settingVisibilityItem; - UM.SettingVisibilityItem { - - // after changing any visibility of settings, set the preset to the "Custom" option - visibilityChangeCallback : function() - { - // If already "Custom" then don't do nothing - if (visibilityPreset.currentIndex != visibilityPreset.model.count - 1) - { - visibilityPreset.currentIndex = visibilityPreset.model.count - 1 - UM.Preferences.setValue("general/preset_setting_visibility_choice", visibilityPreset.model.get(visibilityPreset.currentIndex).text) - } - } - } + UM.SettingVisibilityItem { } } } } \ No newline at end of file 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 615e66277b..235dfac91a 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -15,10 +15,11 @@ Item { id: base; - property Action configureSettings; - property bool findingSettings; - signal showTooltip(Item item, point location, string text); - signal hideTooltip(); + property Action configureSettings + property bool findingSettings + property bool showingAllSettings + signal showTooltip(Item item, point location, string text) + signal hideTooltip() Item { @@ -107,6 +108,57 @@ Item } } + ToolButton + { + id: settingVisibilityMenu + + width: height + height: UM.Theme.getSize("setting_control").height + anchors + { + top: globalProfileRow.bottom + topMargin: UM.Theme.getSize("sidebar_margin").height + right: parent.right + rightMargin: UM.Theme.getSize("sidebar_margin").width + } + style: ButtonStyle + { + background: Item { + UM.RecolorImage { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: UM.Theme.getSize("standard_arrow").width + height: UM.Theme.getSize("standard_arrow").height + sourceSize.width: width + sourceSize.height: width + color: control.enabled ? UM.Theme.getColor("setting_category_text") : UM.Theme.getColor("setting_category_disabled_text") + source: UM.Theme.getIcon("menu") + } + } + label: Label{} + } + menu: SettingVisibilityPresetsMenu + { + showingSearchResults: findingSettings + showingAllSettings: showingAllSettings + + onShowAllSettings: + { + base.showingAllSettings = true; + base.findingSettings = false; + filter.text = ""; + filter.updateDefinitionModel(); + } + onShowSettingVisibilityProfile: + { + base.showingAllSettings = false; + base.findingSettings = false; + filter.text = ""; + filter.updateDefinitionModel(); + } + } + } + Rectangle { id: filterContainer @@ -132,9 +184,9 @@ Item top: globalProfileRow.bottom topMargin: UM.Theme.getSize("sidebar_margin").height left: parent.left - leftMargin: Math.round(UM.Theme.getSize("sidebar_margin").width) - right: parent.right - rightMargin: Math.round(UM.Theme.getSize("sidebar_margin").width) + leftMargin: UM.Theme.getSize("sidebar_margin").width + right: settingVisibilityMenu.left + rightMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) } height: visible ? UM.Theme.getSize("setting_control").height : 0 Behavior on height { NumberAnimation { duration: 100 } } @@ -168,17 +220,9 @@ Item { if(findingSettings) { - expandedCategories = definitionsModel.expanded.slice(); - definitionsModel.expanded = ["*"]; - definitionsModel.showAncestors = true; - definitionsModel.showAll = true; - } - else - { - definitionsModel.expanded = expandedCategories; - definitionsModel.showAncestors = false; - definitionsModel.showAll = false; + showingAllSettings = false; } + updateDefinitionModel(); lastFindingSettings = findingSettings; } } @@ -187,6 +231,27 @@ Item { filter.text = ""; } + + function updateDefinitionModel() + { + if(findingSettings || showingAllSettings) + { + expandedCategories = definitionsModel.expanded.slice(); + definitionsModel.expanded = [""]; // keep categories closed while to prevent render while making settings visible one by one + definitionsModel.showAncestors = true; + definitionsModel.showAll = true; + definitionsModel.expanded = ["*"]; + } + else + { + if(expandedCategories) + { + definitionsModel.expanded = expandedCategories; + } + definitionsModel.showAncestors = false; + definitionsModel.showAll = false; + } + } } MouseArea @@ -209,7 +274,7 @@ Item anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Math.round(UM.Theme.getSize("sidebar_margin").width) + anchors.rightMargin: UM.Theme.getSize("default_margin").width color: UM.Theme.getColor("setting_control_button") hoverColor: UM.Theme.getColor("setting_control_button_hover") @@ -374,8 +439,6 @@ Item key: model.key ? model.key : "" watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ] storeIndex: 0 - // Due to the way setPropertyValue works, removeUnusedValue gives the correct output in case of resolve - removeUnusedValue: model.resolve == undefined } Connections @@ -493,9 +556,17 @@ Item MenuItem { //: Settings context menu action - visible: !findingSettings; + visible: !(findingSettings || showingAllSettings); text: catalog.i18nc("@action:menu", "Hide this setting"); - onTriggered: definitionsModel.hide(contextMenu.key); + onTriggered: + { + definitionsModel.hide(contextMenu.key); + // visible settings have changed, so we're no longer showing a preset + if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) + { + Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + } + } } MenuItem { @@ -511,7 +582,7 @@ Item return catalog.i18nc("@action:menu", "Keep this setting visible"); } } - visible: findingSettings; + visible: (findingSettings || showingAllSettings); onTriggered: { if (contextMenu.settingVisible) @@ -522,6 +593,11 @@ Item { definitionsModel.show(contextMenu.key); } + // visible settings have changed, so we're no longer showing a preset + if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) + { + Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + } } } MenuItem diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index e3107ea7f8..4744bbfda0 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts 1.3 import UM 1.2 as UM import Cura 1.0 as Cura import "Menus" +import "Menus/ConfigurationMenu" Rectangle { @@ -18,6 +19,7 @@ Rectangle property bool hideView: Cura.MachineManager.activeMachineName == "" // Is there an output device for this printer? + property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null @@ -85,12 +87,34 @@ Rectangle } } - MachineSelection { + MachineSelection + { id: machineSelection - width: base.width + width: base.width - configSelection.width - separator.width + height: UM.Theme.getSize("sidebar_header").height + anchors.top: base.top + anchors.left: parent.left + } + + Rectangle + { + id: separator + visible: configSelection.visible + width: visible ? Math.round(UM.Theme.getSize("sidebar_lining_thin").height / 2) : 0 + height: UM.Theme.getSize("sidebar_header").height + color: UM.Theme.getColor("sidebar_lining_thin") + anchors.left: machineSelection.right + } + + ConfigurationSelection + { + id: configSelection + visible: isNetworkPrinter + width: visible ? Math.round(base.width * 0.15) : 0 height: UM.Theme.getSize("sidebar_header").height anchors.top: base.top anchors.right: parent.right + panelWidth: base.width } SidebarHeader { diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 473f4c5cc8..5cd0446b36 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -16,6 +16,8 @@ Column property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex; property bool currentExtruderVisible: extrudersList.visible; + property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 + property bool hasManyPrinterTypes: printerConnected ? Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1 : false spacing: Math.round(UM.Theme.getSize("sidebar_margin").width * 0.9) @@ -24,16 +26,66 @@ Column Item { + id: initialSeparator anchors { left: parent.left right: parent.right } - visible: extruderSelectionRow.visible + visible: printerTypeSelectionRow.visible || buildplateRow.visible || extruderSelectionRow.visible height: UM.Theme.getSize("default_lining").height width: height } + // Printer Type Row + Item + { + id: printerTypeSelectionRow + height: UM.Theme.getSize("sidebar_setup").height + visible: printerConnected && hasManyPrinterTypes && !sidebar.monitoringPrint && !sidebar.hideSettings + + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("sidebar_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("sidebar_margin").width + } + + Label + { + id: configurationLabel + text: catalog.i18nc("@label", "Printer type"); + width: Math.round(parent.width * 0.4 - UM.Theme.getSize("default_margin").width) + height: parent.height + verticalAlignment: Text.AlignVCenter + font: UM.Theme.getFont("default"); + color: UM.Theme.getColor("text"); + } + + ToolButton + { + id: printerTypeSelection + text: Cura.MachineManager.activeMachineDefinitionName + tooltip: Cura.MachineManager.activeMachineDefinitionName + height: UM.Theme.getSize("setting_control").height + width: Math.round(parent.width * 0.7) + UM.Theme.getSize("sidebar_margin").width + anchors.right: parent.right + style: UM.Theme.styles.sidebar_header_button + activeFocusOnPress: true; + + menu: PrinterTypeMenu { } + } + } + + Rectangle { + id: headerSeparator + width: parent.width + visible: printerTypeSelectionRow.visible + height: visible ? UM.Theme.getSize("sidebar_lining").height : 0 + color: UM.Theme.getColor("sidebar_lining") + } + // Extruder Row Item { @@ -91,6 +143,8 @@ Column exclusiveGroup: extruderMenuGroup checked: base.currentExtruderIndex == index + property bool extruder_enabled: true + MouseArea { anchors.fill: parent @@ -102,6 +156,7 @@ Column Cura.ExtruderManager.setActiveExtruderIndex(index); break; case Qt.RightButton: + extruder_enabled = Cura.MachineManager.getExtruder(model.index).isEnabled extruderMenu.popup(); break; } @@ -116,13 +171,13 @@ Column MenuItem { text: catalog.i18nc("@action:inmenu", "Enable Extruder") onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, true) - visible: !Cura.MachineManager.getExtruder(model.index).isEnabled + visible: !extruder_enabled // using an intermediate variable prevents an empty popup that occured now and then } MenuItem { text: catalog.i18nc("@action:inmenu", "Disable Extruder") onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) - visible: Cura.MachineManager.getExtruder(model.index).isEnabled + visible: extruder_enabled } } @@ -258,7 +313,7 @@ Column id: variantRowSpacer height: Math.round(UM.Theme.getSize("sidebar_margin").height / 4) width: height - visible: !extruderSelectionRow.visible + visible: !extruderSelectionRow.visible && !initialSeparator.visible } // Material Row @@ -281,6 +336,8 @@ Column id: materialLabel text: catalog.i18nc("@label", "Material"); width: Math.round(parent.width * 0.45 - UM.Theme.getSize("default_margin").width) + height: parent.height + verticalAlignment: Text.AlignVCenter font: UM.Theme.getFont("default"); color: UM.Theme.getColor("text"); } @@ -289,15 +346,9 @@ Column { id: materialSelection - property var currentRootMaterialName: - { - var materials = Cura.MachineManager.currentRootMaterialName; - var materialName = ""; - if (base.currentExtruderIndex in materials) { - materialName = materials[base.currentExtruderIndex]; - } - return materialName; - } + property var activeExtruder: Cura.MachineManager.activeStack + property var hasActiveExtruder: activeExtruder != null + property var currentRootMaterialName: hasActiveExtruder ? activeExtruder.material.name : "" text: currentRootMaterialName tooltip: currentRootMaterialName @@ -316,7 +367,11 @@ Column property var valueWarning: ! Cura.MachineManager.isActiveQualitySupported function isMaterialSupported () { - return Cura.ContainerManager.getContainerMetaDataEntry(Cura.MachineManager.activeMaterialId, "compatible") == "True" + if (!hasActiveExtruder) + { + return false; + } + return Cura.ContainerManager.getContainerMetaDataEntry(activeExtruder.material.id, "compatible") == "True" } } } @@ -341,6 +396,8 @@ Column id: variantLabel text: Cura.MachineManager.activeDefinitionVariantsName; width: Math.round(parent.width * 0.45 - UM.Theme.getSize("default_margin").width) + height: parent.height + verticalAlignment: Text.AlignVCenter font: UM.Theme.getFont("default"); color: UM.Theme.getColor("text"); } @@ -361,17 +418,14 @@ Column } } - //Buildplate row separator Rectangle { - id: separator - + id: buildplateSeparator + anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width - anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width - anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 2 * UM.Theme.getSize("sidebar_margin").width visible: buildplateRow.visible - width: parent.width - UM.Theme.getSize("sidebar_margin").width * 2 - height: visible ? Math.floor(UM.Theme.getSize("sidebar_lining_thin").height / 2) : 0 - color: UM.Theme.getColor("sidebar_lining_thin") + height: visible ? UM.Theme.getSize("sidebar_lining_thin").height : 0 + color: UM.Theme.getColor("sidebar_lining") } //Buildplate row @@ -394,6 +448,8 @@ Column id: bulidplateLabel text: catalog.i18nc("@label", "Build plate"); width: Math.floor(parent.width * 0.45 - UM.Theme.getSize("default_margin").width) + height: parent.height + verticalAlignment: Text.AlignVCenter font: UM.Theme.getFont("default"); color: UM.Theme.getColor("text"); } diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index 7cc67be03a..41ecb529eb 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -243,6 +243,81 @@ Item anchors.top: parent.top anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + // This Item is used only for tooltip, for slider area which is unavailable + Item + { + function showTooltip (showTooltip) + { + if (showTooltip) { + var content = catalog.i18nc("@tooltip", "This quality profile is not available for you current material and nozzle configuration. Please change these to enable this quality profile") + base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("sidebar_margin").width, customisedSettings.height), content) + } + else { + base.hideTooltip() + } + } + + id: unavailableLineToolTip + height: 20 // hovered area height + z: parent.z + 1 // should be higher, otherwise the area can be hovered + x: 0 + anchors.verticalCenter: qualitySlider.verticalCenter + + Rectangle + { + id: leftArea + width: + { + if (qualityModel.availableTotalTicks == 0) { + return qualityModel.qualitySliderStepWidth * qualityModel.totalTicks + } + return qualityModel.qualitySliderStepWidth * qualityModel.qualitySliderAvailableMin - 10 + } + height: parent.height + color: "transparent" + + MouseArea + { + anchors.fill: parent + hoverEnabled: true + enabled: Cura.SimpleModeSettingsManager.isProfileUserCreated == false + onEntered: unavailableLineToolTip.showTooltip(true) + onExited: unavailableLineToolTip.showTooltip(false) + } + } + + Rectangle + { + id: rightArea + width: { + if(qualityModel.availableTotalTicks == 0) + return 0 + + return qualityModel.qualitySliderMarginRight - 10 + } + height: parent.height + color: "transparent" + x: { + if (qualityModel.availableTotalTicks == 0) { + return 0 + } + + var leftUnavailableArea = qualityModel.qualitySliderStepWidth * qualityModel.qualitySliderAvailableMin + var totalGap = qualityModel.qualitySliderStepWidth * (qualityModel.availableTotalTicks -1) + leftUnavailableArea + 10 + + return totalGap + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: Cura.SimpleModeSettingsManager.isProfileUserCreated == false + onEntered: unavailableLineToolTip.showTooltip(true) + onExited: unavailableLineToolTip.showTooltip(false) + } + } + } + // Draw Unavailable line Rectangle { @@ -516,7 +591,12 @@ Item // Update the slider value to represent the rounded value infillSlider.value = roundedSliderValue - Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) + // 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) { + Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) + } } style: SliderStyle diff --git a/resources/qml/Topbar.qml b/resources/qml/Topbar.qml index 950b9ec12d..69d27d483a 100644 --- a/resources/qml/Topbar.qml +++ b/resources/qml/Topbar.qml @@ -150,7 +150,7 @@ Rectangle visible: base.width - allItemsWidth - 1 * this.width > 0 } - // #5 Left view + // #5 Right view Button { iconSource: UM.Theme.getIcon("view_right") diff --git a/resources/qml/WorkspaceSummaryDialog.qml b/resources/qml/WorkspaceSummaryDialog.qml index 1cfe36d14b..cf19e45fdd 100644 --- a/resources/qml/WorkspaceSummaryDialog.qml +++ b/resources/qml/WorkspaceSummaryDialog.qml @@ -101,7 +101,7 @@ UM.Dialog } Label { - text: Cura.MachineManager.activeMachine.definition.name + text: (Cura.MachineManager.activeMachine == null) ? "" : Cura.MachineManager.activeMachine.definition.name width: (parent.width / 3) | 0 } } @@ -173,7 +173,7 @@ UM.Dialog } Label { - text: Cura.MachineManager.activeVariantNames[modelData] + ", " + Cura.MachineManager.currentRootMaterialName[modelData] + text: Cura.MachineManager.activeVariantNames[modelData] + ", " + Cura.MachineManager.getExtruder(modelData).material.name width: (parent.width / 3) | 0 } } diff --git a/resources/quality/ultimaker3/um3_aa0.25_PC_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.25_PC_Normal_Quality.inst.cfg index 657e194398..bd95837fee 100644 --- a/resources/quality/ultimaker3/um3_aa0.25_PC_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.25_PC_Normal_Quality.inst.cfg @@ -34,7 +34,6 @@ raft_airgap = 0.25 raft_interface_thickness = =max(layer_height * 1.5, 0.225) retraction_count_max = 80 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.25_PP_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.25_PP_Normal_Quality.inst.cfg index 694f4a56d6..6bdd94f58f 100644 --- a/resources/quality/ultimaker3/um3_aa0.25_PP_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.25_PP_Normal_Quality.inst.cfg @@ -38,7 +38,6 @@ retraction_count_max = 45 retraction_extra_prime_amount = 0.2 retraction_extrusion_window = 6.5 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 13 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PC_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PC_Draft_Print.inst.cfg index f0a420ff5c..79ae457587 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PC_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PC_Draft_Print.inst.cfg @@ -44,7 +44,6 @@ raft_interface_thickness = =max(layer_height * 1.5, 0.225) retraction_count_max = 80 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PC_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PC_Fast_Print.inst.cfg index eba8b84a1e..2e07e65bbe 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PC_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PC_Fast_Print.inst.cfg @@ -43,7 +43,6 @@ raft_interface_thickness = =max(layer_height * 1.5, 0.225) retraction_count_max = 80 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PC_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PC_High_Quality.inst.cfg index 833c771b6e..2b1b1f850b 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PC_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PC_High_Quality.inst.cfg @@ -44,7 +44,6 @@ raft_interface_thickness = =max(layer_height * 1.5, 0.225) retraction_count_max = 80 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PC_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PC_Normal_Quality.inst.cfg index a6290a4c65..437ef43878 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PC_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PC_Normal_Quality.inst.cfg @@ -41,7 +41,6 @@ raft_interface_thickness = =max(layer_height * 1.5, 0.225) retraction_count_max = 80 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PP_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PP_Draft_Print.inst.cfg index 3f288618b7..1db33d9073 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PP_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PP_Draft_Print.inst.cfg @@ -45,7 +45,6 @@ retraction_count_max = 12 retraction_extra_prime_amount = 0.8 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 18 @@ -61,7 +60,6 @@ support_angle = 50 switch_extruder_prime_speed = 15 switch_extruder_retraction_amount = 20 switch_extruder_retraction_speeds = 35 -travel_avoid_distance = 3 wall_0_inset = 0 wall_line_width_x = =line_width wall_thickness = =line_width * 3 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PP_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PP_Fast_Print.inst.cfg index d4f84fc192..fe7da92bbe 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PP_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PP_Fast_Print.inst.cfg @@ -44,7 +44,6 @@ retraction_count_max = 12 retraction_extra_prime_amount = 0.8 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 18 @@ -61,7 +60,6 @@ switch_extruder_prime_speed = 15 switch_extruder_retraction_amount = 20 switch_extruder_retraction_speeds = 35 top_bottom_thickness = 1.1 -travel_avoid_distance = 3 wall_0_inset = 0 wall_line_width_x = =line_width wall_thickness = =line_width * 3 diff --git a/resources/quality/ultimaker3/um3_aa0.4_PP_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PP_Normal_Quality.inst.cfg index 329e84a531..8bbb0118b5 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PP_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PP_Normal_Quality.inst.cfg @@ -43,7 +43,6 @@ retraction_count_max = 12 retraction_extra_prime_amount = 0.8 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 0.8 retraction_prime_speed = 18 @@ -60,7 +59,6 @@ switch_extruder_prime_speed = 15 switch_extruder_retraction_amount = 20 switch_extruder_retraction_speeds = 35 top_bottom_thickness = 1 -travel_avoid_distance = 3 wall_0_inset = 0 wall_line_width_x = =line_width wall_thickness = =line_width * 3 diff --git a/resources/quality/ultimaker3/um3_aa0.4_TPU_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_TPU_Draft_Print.inst.cfg index cace7e902e..25e7c2c0ab 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_TPU_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_TPU_Draft_Print.inst.cfg @@ -43,7 +43,6 @@ retraction_count_max = 12 retraction_extra_prime_amount = 0.8 retraction_extrusion_window = 1 retraction_hop = 1.5 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = =line_width * 2 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.4_TPU_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_TPU_Fast_Print.inst.cfg index 7dae439fab..0b7fa6501e 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_TPU_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_TPU_Fast_Print.inst.cfg @@ -44,7 +44,6 @@ retraction_count_max = 12 retraction_extra_prime_amount = 0.8 retraction_extrusion_window = 1 retraction_hop = 1.5 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = =line_width * 2 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.4_TPU_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_TPU_Normal_Quality.inst.cfg index 71796eabc6..9afecb8987 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_TPU_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_TPU_Normal_Quality.inst.cfg @@ -41,7 +41,6 @@ retraction_count_max = 12 retraction_extra_prime_amount = 0.8 retraction_extrusion_window = 1 retraction_hop = 1.5 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = =line_width * 2 retraction_prime_speed = 15 diff --git a/resources/quality/ultimaker3/um3_aa0.8_CPEP_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.8_CPEP_Fast_Print.inst.cfg index c958cdd628..87728c2297 100644 --- a/resources/quality/ultimaker3/um3_aa0.8_CPEP_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.8_CPEP_Fast_Print.inst.cfg @@ -36,4 +36,3 @@ support_bottom_distance = =support_z_distance support_line_width = =round(line_width * 0.6 / 0.7, 2) support_z_distance = =layer_height top_bottom_thickness = 1.2 -travel_avoid_distance = 1.5 diff --git a/resources/quality/ultimaker3/um3_aa0.8_CPEP_Superdraft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.8_CPEP_Superdraft_Print.inst.cfg index 6a726c6079..603f32a688 100644 --- a/resources/quality/ultimaker3/um3_aa0.8_CPEP_Superdraft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.8_CPEP_Superdraft_Print.inst.cfg @@ -37,4 +37,3 @@ support_bottom_distance = =support_z_distance support_line_width = =round(line_width * 0.6 / 0.7, 2) support_z_distance = =layer_height top_bottom_thickness = 1.2 -travel_avoid_distance = 1.5 diff --git a/resources/quality/ultimaker3/um3_aa0.8_CPEP_Verydraft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.8_CPEP_Verydraft_Print.inst.cfg index 978f4fd732..432d502245 100644 --- a/resources/quality/ultimaker3/um3_aa0.8_CPEP_Verydraft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.8_CPEP_Verydraft_Print.inst.cfg @@ -37,4 +37,3 @@ support_bottom_distance = =support_z_distance support_line_width = =round(line_width * 0.6 / 0.7, 2) support_z_distance = =layer_height top_bottom_thickness = 1.2 -travel_avoid_distance = 1.5 diff --git a/resources/quality/ultimaker3/um3_aa0.8_PC_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.8_PC_Fast_Print.inst.cfg index b128fc952a..d8460c6971 100644 --- a/resources/quality/ultimaker3/um3_aa0.8_PC_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.8_PC_Fast_Print.inst.cfg @@ -29,4 +29,3 @@ speed_topbottom = =math.ceil(speed_print * 25 / 50) speed_wall = =math.ceil(speed_print * 40 / 50) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) support_line_width = =round(line_width * 0.6 / 0.7, 2) -travel_avoid_distance = 3 diff --git a/resources/quality/ultimaker3/um3_aa0.8_PC_Superdraft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.8_PC_Superdraft_Print.inst.cfg index d208afb2e1..53f613ec70 100644 --- a/resources/quality/ultimaker3/um3_aa0.8_PC_Superdraft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.8_PC_Superdraft_Print.inst.cfg @@ -29,4 +29,3 @@ speed_topbottom = =math.ceil(speed_print * 25 / 50) speed_wall = =math.ceil(speed_print * 40 / 50) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) support_line_width = =round(line_width * 0.6 / 0.7, 2) -travel_avoid_distance = 3 diff --git a/resources/quality/ultimaker3/um3_aa0.8_PC_Verydraft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.8_PC_Verydraft_Print.inst.cfg index fbbf9876e0..a4fdaaa791 100644 --- a/resources/quality/ultimaker3/um3_aa0.8_PC_Verydraft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.8_PC_Verydraft_Print.inst.cfg @@ -30,4 +30,3 @@ speed_topbottom = =math.ceil(speed_print * 25 / 50) speed_wall = =math.ceil(speed_print * 40 / 50) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) support_line_width = =round(line_width * 0.6 / 0.7, 2) -travel_avoid_distance = 3 diff --git a/resources/preset_setting_visibility_groups/advanced.cfg b/resources/setting_visibility/advanced.cfg similarity index 100% rename from resources/preset_setting_visibility_groups/advanced.cfg rename to resources/setting_visibility/advanced.cfg diff --git a/resources/preset_setting_visibility_groups/basic.cfg b/resources/setting_visibility/basic.cfg similarity index 100% rename from resources/preset_setting_visibility_groups/basic.cfg rename to resources/setting_visibility/basic.cfg diff --git a/resources/preset_setting_visibility_groups/expert.cfg b/resources/setting_visibility/expert.cfg similarity index 100% rename from resources/preset_setting_visibility_groups/expert.cfg rename to resources/setting_visibility/expert.cfg diff --git a/resources/themes/cura-light/icons/buildplate.svg b/resources/themes/cura-light/icons/buildplate.svg new file mode 100644 index 0000000000..9e61296958 --- /dev/null +++ b/resources/themes/cura-light/icons/buildplate.svg @@ -0,0 +1,17 @@ + + + + icn_buildplate + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/connected.svg b/resources/themes/cura-light/icons/connected.svg new file mode 100644 index 0000000000..18423bb6c4 --- /dev/null +++ b/resources/themes/cura-light/icons/connected.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/disconnected.svg b/resources/themes/cura-light/icons/disconnected.svg new file mode 100644 index 0000000000..019dff117e --- /dev/null +++ b/resources/themes/cura-light/icons/disconnected.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/menu.svg b/resources/themes/cura-light/icons/menu.svg new file mode 100644 index 0000000000..85fbfb072c --- /dev/null +++ b/resources/themes/cura-light/icons/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/themes/cura-light/icons/printer_group.svg b/resources/themes/cura-light/icons/printer_group.svg new file mode 100644 index 0000000000..614bea90b8 --- /dev/null +++ b/resources/themes/cura-light/icons/printer_group.svg @@ -0,0 +1,12 @@ + + + + icn_groupPrinters + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/printer_single.svg b/resources/themes/cura-light/icons/printer_single.svg new file mode 100644 index 0000000000..f7dc83987d --- /dev/null +++ b/resources/themes/cura-light/icons/printer_single.svg @@ -0,0 +1,14 @@ + + + + icn_singlePrinter + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 8c8e6d1c47..c0b71ac618 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -14,6 +14,16 @@ "weight": 50, "family": "Noto Sans" }, + "medium": { + "size": 1.16, + "weight": 50, + "family": "Noto Sans" + }, + "medium_bold": { + "size": 1.16, + "weight": 63, + "family": "Noto Sans" + }, "default": { "size": 1.0, "weight": 50, @@ -289,7 +299,21 @@ "layerview_move_combing": [0, 0, 255, 255], "layerview_move_retraction": [128, 128, 255, 255], "layerview_support_interface": [64, 192, 255, 255], - "layerview_nozzle": [181, 166, 66, 50] + "layerview_nozzle": [181, 166, 66, 50], + + "configuration_item": [255, 255, 255, 0], + "configuration_item_active": [12, 169, 227, 32], + "configuration_item_text": [0, 0, 0, 255], + "configuration_item_text_active": [0, 0, 0, 255], + "configuration_item_border": [127, 127, 127, 255], + "configuration_item_border_active": [12, 169, 227, 32], + "configuration_item_border_hover": [12, 169, 227, 255], + + "tab_status_connected": [12, 169, 227, 255], + "tab_status_disconnected": [200, 200, 200, 255], + + "printer_config_matched": [12, 169, 227, 255], + "printer_config_mismatch": [127, 127, 127, 255] }, "sizes": { @@ -342,6 +366,9 @@ "small_button": [2, 2], "small_button_icon": [1.5, 1.5], + "printer_status_icon": [1.8, 1.8], + "printer_sync_icon": [1.2, 1.2], + "topbar_logo_right_margin": [3, 0], "topbar_button": [8, 4], "topbar_button_icon": [1.2, 1.2], diff --git a/resources/variants/ultimaker3_aa0.8.inst.cfg b/resources/variants/ultimaker3_aa0.8.inst.cfg index 9f011b9164..0bccf91b7c 100644 --- a/resources/variants/ultimaker3_aa0.8.inst.cfg +++ b/resources/variants/ultimaker3_aa0.8.inst.cfg @@ -45,7 +45,6 @@ retraction_amount = 6.5 retraction_count_max = 25 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True skin_overlap = 5 speed_equalize_flow_enabled = True diff --git a/resources/variants/ultimaker3_aa04.inst.cfg b/resources/variants/ultimaker3_aa04.inst.cfg index d62e0e0d24..0e0187e4df 100644 --- a/resources/variants/ultimaker3_aa04.inst.cfg +++ b/resources/variants/ultimaker3_aa04.inst.cfg @@ -12,6 +12,7 @@ hardware_type = nozzle brim_width = 7 machine_nozzle_cool_down_speed = 0.9 machine_nozzle_id = AA 0.4 +machine_nozzle_tip_outer_diameter = 1.0 raft_acceleration = =acceleration_print raft_airgap = 0.3 raft_base_thickness = =resolveOrValue('layer_height_0') * 1.2 diff --git a/resources/variants/ultimaker3_bb0.8.inst.cfg b/resources/variants/ultimaker3_bb0.8.inst.cfg index 41c6419ec1..ef6dc625ac 100644 --- a/resources/variants/ultimaker3_bb0.8.inst.cfg +++ b/resources/variants/ultimaker3_bb0.8.inst.cfg @@ -56,7 +56,6 @@ retraction_amount = 4.5 retraction_count_max = 15 retraction_extrusion_window = =retraction_amount retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 5 retraction_prime_speed = 15 diff --git a/resources/variants/ultimaker3_bb04.inst.cfg b/resources/variants/ultimaker3_bb04.inst.cfg index 529cabcc95..91e70f9f98 100644 --- a/resources/variants/ultimaker3_bb04.inst.cfg +++ b/resources/variants/ultimaker3_bb04.inst.cfg @@ -20,6 +20,7 @@ jerk_support_interface = =math.ceil(jerk_support * 10 / 15) jerk_support_bottom = =math.ceil(jerk_support_interface * 1 / 10) machine_nozzle_heat_up_speed = 1.5 machine_nozzle_id = BB 0.4 +machine_nozzle_tip_outer_diameter = 1.0 prime_tower_purge_volume = 1 raft_base_speed = 20 raft_interface_speed = 20 diff --git a/resources/variants/ultimaker3_extended_aa0.8.inst.cfg b/resources/variants/ultimaker3_extended_aa0.8.inst.cfg index 2dfd64a94b..184416b06e 100644 --- a/resources/variants/ultimaker3_extended_aa0.8.inst.cfg +++ b/resources/variants/ultimaker3_extended_aa0.8.inst.cfg @@ -45,7 +45,6 @@ retraction_amount = 6.5 retraction_count_max = 25 retraction_extrusion_window = 1 retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True skin_overlap = 5 speed_equalize_flow_enabled = True diff --git a/resources/variants/ultimaker3_extended_aa04.inst.cfg b/resources/variants/ultimaker3_extended_aa04.inst.cfg index 9ac7e1fdf9..8a2f061224 100644 --- a/resources/variants/ultimaker3_extended_aa04.inst.cfg +++ b/resources/variants/ultimaker3_extended_aa04.inst.cfg @@ -12,6 +12,7 @@ hardware_type = nozzle brim_width = 7 machine_nozzle_cool_down_speed = 0.9 machine_nozzle_id = AA 0.4 +machine_nozzle_tip_outer_diameter = 1.0 raft_acceleration = =acceleration_print raft_airgap = 0.3 raft_base_thickness = =resolveOrValue('layer_height_0') * 1.2 diff --git a/resources/variants/ultimaker3_extended_bb0.8.inst.cfg b/resources/variants/ultimaker3_extended_bb0.8.inst.cfg index 42d7d85728..8a1a9373f3 100644 --- a/resources/variants/ultimaker3_extended_bb0.8.inst.cfg +++ b/resources/variants/ultimaker3_extended_bb0.8.inst.cfg @@ -56,7 +56,6 @@ retraction_amount = 4.5 retraction_count_max = 15 retraction_extrusion_window = =retraction_amount retraction_hop = 2 -retraction_hop_enabled = True retraction_hop_only_when_collides = True retraction_min_travel = 5 retraction_prime_speed = 15 diff --git a/resources/variants/ultimaker3_extended_bb04.inst.cfg b/resources/variants/ultimaker3_extended_bb04.inst.cfg index 958180ede5..5b35351312 100644 --- a/resources/variants/ultimaker3_extended_bb04.inst.cfg +++ b/resources/variants/ultimaker3_extended_bb04.inst.cfg @@ -20,6 +20,7 @@ jerk_support_interface = =math.ceil(jerk_support * 10 / 15) jerk_support_bottom = =math.ceil(jerk_support_interface * 1 / 10) machine_nozzle_heat_up_speed = 1.5 machine_nozzle_id = BB 0.4 +machine_nozzle_tip_outer_diameter = 1.0 prime_tower_purge_volume = 1 raft_base_speed = 20 raft_interface_speed = 20 diff --git a/tests/TestProfileRequirements.py b/tests/TestProfileRequirements.py index a91a08172c..edeec909f2 100644 --- a/tests/TestProfileRequirements.py +++ b/tests/TestProfileRequirements.py @@ -22,4 +22,4 @@ def test_ultimaker3extended_variants(um3_file, um3e_file): um3.read_file(open(os.path.join(directory, um3_file))) um3e = configparser.ConfigParser() um3e.read_file(open(os.path.join(directory, um3e_file))) - assert um3["values"] == um3e["values"] \ No newline at end of file + assert um3["values"] == um3e["values"] diff --git a/tools/check_preset_settings.py b/tools/check_preset_settings.py old mode 100755 new mode 100644