diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index 505c988a13..6c95dc2c92 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -435,6 +435,18 @@ Cura.MachineAction property bool allowNegative: true } + Loader + { + id: extruderCoolingFanNumberField + sourceComponent: numericTextFieldWithUnit + property string settingKey: "machine_extruder_cooling_fan_number" + property string label: catalog.i18nc("@label", "Cooling Fan Number") + property string unit: catalog.i18nc("@label", "") + property bool isExtruderSetting: true + property bool forceUpdateOnChange: true + property bool allowNegative: false + } + Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height } Row diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index b28a028325..1a1ea92d10 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -2,6 +2,7 @@ # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot +from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources @@ -9,55 +10,62 @@ from UM.Application import Application from UM.Extension import Extension from UM.Logger import Logger -import configparser #The script lists are stored in metadata as serialised config files. -import io #To allow configparser to write to a string. +import configparser # The script lists are stored in metadata as serialised config files. +import io # To allow configparser to write to a string. import os.path import pkgutil import sys import importlib.util from UM.i18n import i18nCatalog +from cura.CuraApplication import CuraApplication + i18n_catalog = i18nCatalog("cura") +if TYPE_CHECKING: + from .Script import Script + ## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated # g-code files. class PostProcessingPlugin(QObject, Extension): - def __init__(self, parent = None): - super().__init__(parent) + def __init__(self, parent = None) -> None: + QObject.__init__(self, parent) + Extension.__init__(self) self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) self._view = None # Loaded scripts are all scripts that can be used - self._loaded_scripts = {} - self._script_labels = {} + self._loaded_scripts = {} # type: Dict[str, Type[Script]] + self._script_labels = {} # type: Dict[str, str] # Script list contains instances of scripts in loaded_scripts. # There can be duplicates, which will be executed in sequence. - self._script_list = [] + self._script_list = [] # type: List[Script] self._selected_script_index = -1 Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) #When the current printer changes, update the list of scripts. - Application.getInstance().mainWindowChanged.connect(self._createView) #When the main window is created, create the view so that we can display the post-processing icon if necessary. + Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts. + CuraApplication.getInstance().mainWindowChanged.connect(self._createView) # When the main window is created, create the view so that we can display the post-processing icon if necessary. selectedIndexChanged = pyqtSignal() - @pyqtProperty("QVariant", notify = selectedIndexChanged) - def selectedScriptDefinitionId(self): + + @pyqtProperty(str, notify = selectedIndexChanged) + def selectedScriptDefinitionId(self) -> Optional[str]: try: return self._script_list[self._selected_script_index].getDefinitionId() except: return "" - @pyqtProperty("QVariant", notify=selectedIndexChanged) - def selectedScriptStackId(self): + @pyqtProperty(str, notify=selectedIndexChanged) + def selectedScriptStackId(self) -> Optional[str]: try: return self._script_list[self._selected_script_index].getStackId() except: return "" ## Execute all post-processing scripts on the gcode. - def execute(self, output_device): + def execute(self, output_device) -> None: scene = Application.getInstance().getController().getScene() # If the scene does not have a gcode, do nothing if not hasattr(scene, "gcode_dict"): @@ -67,7 +75,7 @@ class PostProcessingPlugin(QObject, Extension): return # get gcode list for the active build plate - active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: return @@ -86,16 +94,17 @@ class PostProcessingPlugin(QObject, Extension): Logger.log("e", "Already post processed") @pyqtSlot(int) - def setSelectedScriptIndex(self, index): - self._selected_script_index = index - self.selectedIndexChanged.emit() + def setSelectedScriptIndex(self, index: int) -> None: + if self._selected_script_index != index: + self._selected_script_index = index + self.selectedIndexChanged.emit() @pyqtProperty(int, notify = selectedIndexChanged) - def selectedScriptIndex(self): + def selectedScriptIndex(self) -> int: return self._selected_script_index @pyqtSlot(int, int) - def moveScript(self, index, new_index): + def moveScript(self, index: int, new_index: int) -> None: if new_index < 0 or new_index > len(self._script_list) - 1: return # nothing needs to be done else: @@ -107,7 +116,7 @@ class PostProcessingPlugin(QObject, Extension): ## Remove a script from the active script list by index. @pyqtSlot(int) - def removeScriptByIndex(self, index): + def removeScriptByIndex(self, index: int) -> None: self._script_list.pop(index) if len(self._script_list) - 1 < self._selected_script_index: self._selected_script_index = len(self._script_list) - 1 @@ -118,14 +127,16 @@ class PostProcessingPlugin(QObject, Extension): ## Load all scripts from all paths where scripts can be found. # # This should probably only be done on init. - def loadAllScripts(self): - if self._loaded_scripts: #Already loaded. + def loadAllScripts(self) -> None: + if self._loaded_scripts: # Already loaded. return - #The PostProcessingPlugin path is for built-in scripts. - #The Resources path is where the user should store custom scripts. - #The Preferences path is legacy, where the user may previously have stored scripts. + # The PostProcessingPlugin path is for built-in scripts. + # The Resources path is where the user should store custom scripts. + # The Preferences path is legacy, where the user may previously have stored scripts. for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]: + if root is None: + continue path = os.path.join(root, "scripts") if not os.path.isdir(path): try: @@ -139,7 +150,7 @@ class PostProcessingPlugin(QObject, Extension): ## Load all scripts from provided path. # This should probably only be done on init. # \param path Path to check for scripts. - def loadScripts(self, path): + def loadScripts(self, path: str) -> None: ## Load all scripts in the scripts folders scripts = pkgutil.iter_modules(path = [path]) for loader, script_name, ispkg in scripts: @@ -148,6 +159,8 @@ class PostProcessingPlugin(QObject, Extension): try: spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) loaded_script = importlib.util.module_from_spec(spec) + if spec.loader is None: + continue spec.loader.exec_module(loaded_script) sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name? @@ -172,23 +185,24 @@ class PostProcessingPlugin(QObject, Extension): loadedScriptListChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = loadedScriptListChanged) - def loadedScriptList(self): + def loadedScriptList(self) -> List[str]: return sorted(list(self._loaded_scripts.keys())) @pyqtSlot(str, result = str) - def getScriptLabelByKey(self, key): - return self._script_labels[key] + def getScriptLabelByKey(self, key: str) -> Optional[str]: + return self._script_labels.get(key) scriptListChanged = pyqtSignal() - @pyqtProperty("QVariantList", notify = scriptListChanged) - def scriptList(self): + @pyqtProperty("QStringList", notify = scriptListChanged) + def scriptList(self) -> List[str]: script_list = [script.getSettingData()["key"] for script in self._script_list] return script_list @pyqtSlot(str) - def addScriptToList(self, key): + def addScriptToList(self, key: str) -> None: Logger.log("d", "Adding script %s to list.", key) new_script = self._loaded_scripts[key]() + new_script.initialize() self._script_list.append(new_script) self.setSelectedScriptIndex(len(self._script_list) - 1) self.scriptListChanged.emit() @@ -196,81 +210,89 @@ class PostProcessingPlugin(QObject, Extension): ## When the global container stack is changed, swap out the list of active # scripts. - def _onGlobalContainerStackChanged(self): + def _onGlobalContainerStackChanged(self) -> None: self.loadAllScripts() new_stack = Application.getInstance().getGlobalContainerStack() + if new_stack is None: + return self._script_list.clear() - if not new_stack.getMetaDataEntry("post_processing_scripts"): #Missing or empty. - self.scriptListChanged.emit() #Even emit this if it didn't change. We want it to write the empty list to the stack's metadata. + if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty. + self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata. return self._script_list.clear() scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts") - for script_str in scripts_list_strs.split("\n"): #Encoded config files should never contain three newlines in a row. At most 2, just before section headers. - if not script_str: #There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here). + for script_str in scripts_list_strs.split("\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers. + if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here). continue - script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") #Unescape escape sequences. + script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences. script_parser = configparser.ConfigParser(interpolation = None) - script_parser.optionxform = str #Don't transform the setting keys as they are case-sensitive. + script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive. script_parser.read_string(script_str) - for script_name, settings in script_parser.items(): #There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. - if script_name == "DEFAULT": #ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. + for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. + if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. continue - if script_name not in self._loaded_scripts: #Don't know this post-processing plug-in. + if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in. Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name)) continue new_script = self._loaded_scripts[script_name]() - for setting_key, setting_value in settings.items(): #Put all setting values into the script. - new_script._instance.setProperty(setting_key, "value", setting_value) + new_script.initialize() + for setting_key, setting_value in settings.items(): # Put all setting values into the script. + if new_script._instance is not None: + new_script._instance.setProperty(setting_key, "value", setting_value) self._script_list.append(new_script) self.setSelectedScriptIndex(0) self.scriptListChanged.emit() @pyqtSlot() - def writeScriptsToStack(self): - script_list_strs = [] + def writeScriptsToStack(self) -> None: + script_list_strs = [] # type: List[str] for script in self._script_list: - parser = configparser.ConfigParser(interpolation = None) #We'll encode the script as a config with one section. The section header is the key and its values are the settings. - parser.optionxform = str #Don't transform the setting keys as they are case-sensitive. + parser = configparser.ConfigParser(interpolation = None) # We'll encode the script as a config with one section. The section header is the key and its values are the settings. + parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive. script_name = script.getSettingData()["key"] parser.add_section(script_name) for key in script.getSettingData()["settings"]: value = script.getSettingValueByKey(key) parser[script_name][key] = str(value) - serialized = io.StringIO() #ConfigParser can only write to streams. Fine. + serialized = io.StringIO() # ConfigParser can only write to streams. Fine. parser.write(serialized) serialized.seek(0) script_str = serialized.read() - script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") #Escape newlines because configparser sees those as section delimiters. + script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") # Escape newlines because configparser sees those as section delimiters. script_list_strs.append(script_str) - script_list_strs = "\n".join(script_list_strs) #ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter. + script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter. global_stack = Application.getInstance().getGlobalContainerStack() + if global_stack is None: + return + if "post_processing_scripts" not in global_stack.getMetaData(): global_stack.setMetaDataEntry("post_processing_scripts", "") - Application.getInstance().getGlobalContainerStack().setMetaDataEntry("post_processing_scripts", script_list_strs) + + global_stack.setMetaDataEntry("post_processing_scripts", script_list_string) ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. - def _createView(self): + def _createView(self) -> None: Logger.log("d", "Creating post processing plugin view.") self.loadAllScripts() # Create the plugin dialog component - path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") - self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) + path = os.path.join(cast(str, PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin")), "PostProcessingPlugin.qml") + self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) if self._view is None: Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.") return Logger.log("d", "Post processing view created.") # Create the save button component - Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) + CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) ## Show the (GUI) popup of the post processing plugin. - def showPopup(self): + def showPopup(self) -> None: if self._view is None: self._createView() if self._view is None: @@ -282,8 +304,9 @@ class PostProcessingPlugin(QObject, Extension): # To do this we use the global container stack propertyChanged. # Re-slicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode - def _propertyChanged(self): + def _propertyChanged(self) -> None: global_container_stack = Application.getInstance().getGlobalContainerStack() - global_container_stack.propertyChanged.emit("post_processing_plugin", "value") + if global_container_stack is not None: + global_container_stack.propertyChanged.emit("post_processing_plugin", "value") diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py index 7e430a5c78..e502f107f9 100644 --- a/plugins/PostProcessingPlugin/Script.py +++ b/plugins/PostProcessingPlugin/Script.py @@ -1,6 +1,8 @@ # Copyright (c) 2015 Jaime van Kessel # Copyright (c) 2018 Ultimaker B.V. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from typing import Optional, Any, Dict, TYPE_CHECKING, List + from UM.Signal import Signal, signalemitter from UM.i18n import i18nCatalog @@ -17,23 +19,27 @@ import json import collections i18n_catalog = i18nCatalog("cura") +if TYPE_CHECKING: + from UM.Settings.Interfaces import DefinitionContainerInterface + ## Base class for scripts. All scripts should inherit the script class. @signalemitter class Script: - def __init__(self): + def __init__(self) -> None: super().__init__() - self._settings = None - self._stack = None + self._stack = None # type: Optional[ContainerStack] + self._definition = None # type: Optional[DefinitionContainerInterface] + self._instance = None # type: Optional[InstanceContainer] + def initialize(self) -> None: setting_data = self.getSettingData() - self._stack = ContainerStack(stack_id = str(id(self))) + self._stack = ContainerStack(stack_id=str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. - ## Check if the definition of this script already exists. If not, add it to the registry. if "key" in setting_data: - definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"]) + definitions = ContainerRegistry.getInstance().findDefinitionContainers(id=setting_data["key"]) if definitions: # Definition was found self._definition = definitions[0] @@ -45,10 +51,13 @@ class Script: except ContainerFormatError: self._definition = None return + if self._definition is None: + return self._stack.addContainer(self._definition) self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) - self._instance.setMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0)) + self._instance.setMetaDataEntry("setting_version", + self._definition.getMetaDataEntry("setting_version", default=0)) self._stack.addContainer(self._instance) self._stack.propertyChanged.connect(self._onPropertyChanged) @@ -57,16 +66,17 @@ class Script: settingsLoaded = Signal() valueChanged = Signal() # Signal emitted whenever a value of a setting is changed - def _onPropertyChanged(self, key, property_name): + def _onPropertyChanged(self, key: str, property_name: str) -> None: if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. - # Reslicing is necessary for setting changes in this plugin, because the changes + # Re-slicing is necessary for setting changes in this plugin, because the changes # are applied only once per "fresh" gcode global_container_stack = Application.getInstance().getGlobalContainerStack() - global_container_stack.propertyChanged.emit(key, property_name) + if global_container_stack is not None: + global_container_stack.propertyChanged.emit(key, property_name) ## Needs to return a dict that can be used to construct a settingcategory file. # See the example script for an example. @@ -74,30 +84,35 @@ class Script: # Scripts can either override getSettingData directly, or use getSettingDataString # to return a string that will be parsed as json. The latter has the benefit over # returning a dict in that the order of settings is maintained. - def getSettingData(self): - setting_data = self.getSettingDataString() - if type(setting_data) == str: - setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict) + def getSettingData(self) -> Dict[str, Any]: + setting_data_as_string = self.getSettingDataString() + setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) return setting_data - def getSettingDataString(self): + def getSettingDataString(self) -> str: raise NotImplementedError() - def getDefinitionId(self): + def getDefinitionId(self) -> Optional[str]: if self._stack: - return self._stack.getBottom().getId() + bottom = self._stack.getBottom() + if bottom is not None: + return bottom.getId() + return None - def getStackId(self): + def getStackId(self) -> Optional[str]: if self._stack: return self._stack.getId() + return None ## Convenience function that retrieves value of a setting from the stack. - def getSettingValueByKey(self, key): - return self._stack.getProperty(key, "value") + def getSettingValueByKey(self, key: str) -> Any: + if self._stack is not None: + return self._stack.getProperty(key, "value") + return None ## Convenience function that finds the value in a line of g-code. # When requesting key = x from line "G1 X100" the value 100 is returned. - def getValue(self, line, key, default = None): + def getValue(self, line: str, key: str, default = None) -> Any: if not key in line or (';' in line and line.find(key) > line.find(';')): return default sub_part = line[line.find(key) + 1:] @@ -125,7 +140,7 @@ class Script: # \param line The original g-code line that must be modified. If not # provided, an entirely new g-code line will be produced. # \return A line of g-code with the desired parameters filled in. - def putValue(self, line = "", **kwargs): + def putValue(self, line: str = "", **kwargs) -> str: #Strip the comment. comment = "" if ";" in line: @@ -166,5 +181,5 @@ class Script: ## This is called when the script is executed. # It gets a list of g-code strings and needs to return a (modified) list. - def execute(self, data): + def execute(self, data: List[str]) -> List[str]: raise NotImplementedError() diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index aafbca0247..0ae8b4d9e4 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -24,7 +24,7 @@ from UM.Signal import Signal from UM.View.CompositePass import CompositePass from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGLContext import OpenGLContext - +from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.View import View from UM.i18n import i18nCatalog @@ -42,8 +42,6 @@ from typing import Optional, TYPE_CHECKING, List, cast if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode from UM.Scene.Scene import Scene - from UM.View.GL.ShaderProgram import ShaderProgram - from UM.View.RenderPass import RenderPass from UM.Settings.ContainerStack import ContainerStack catalog = i18nCatalog("cura") diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml index e9aaf39226..cba55051f5 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml @@ -126,7 +126,7 @@ Item return "" } var date = new Date(details.last_updated) - return date.toLocaleDateString(UM.Preferences.getValue("general/language")) + return date.toLocaleString(UM.Preferences.getValue("general/language")) } font: UM.Theme.getFont("very_small") color: UM.Theme.getColor("text") diff --git a/resources/definitions/fdmextruder.def.json b/resources/definitions/fdmextruder.def.json index 3f84ed69a4..19c9e92d18 100644 --- a/resources/definitions/fdmextruder.def.json +++ b/resources/definitions/fdmextruder.def.json @@ -178,7 +178,19 @@ "maximum_value": "machine_height", "settable_per_mesh": false, "settable_per_extruder": true - } + }, + "machine_extruder_cooling_fan_number": + { + "label": "Extruder Print Cooling Fan", + "description": "The number of the print cooling fan associated with this extruder. Only change this from the default value of 0 when you have a different print cooling fan for each extruder.", + "type": "int", + "default_value": 0, + "minimum_value": "0", + "settable_per_mesh": false, + "settable_per_extruder": true, + "settable_per_meshgroup": false, + "setttable_globally": false + } } }, "platform_adhesion": diff --git a/resources/i18n/pl_PL/fdmprinter.def.json.po b/resources/i18n/pl_PL/fdmprinter.def.json.po index 53aa32009e..a8b07e032c 100644 --- a/resources/i18n/pl_PL/fdmprinter.def.json.po +++ b/resources/i18n/pl_PL/fdmprinter.def.json.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Cura 3.5\n" -"Report-Msgid-Bugs-To: r.dulek@ultimaker.com +"Report-Msgid-Bugs-To: r.dulek@ultimaker.com" "POT-Creation-Date: 2018-09-19 17:07+0000\n" "PO-Revision-Date: 2018-09-21 21:52+0200\n" "Last-Translator: 'Jaguś' Paweł Jagusiak, Andrzej 'anraf1001' Rafalski and Jakub 'drzejkopf' Świeciński\n"