Merge branch 'master' of github.com:Ultimaker/Cura

This commit is contained in:
Diego Prado Gesto 2018-03-26 14:15:10 +02:00
commit 689f208615
3 changed files with 151 additions and 20 deletions

View File

@ -1,8 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Dict
from UM.Logger import Logger from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
from cura.Machines.MaterialNode import MaterialNode
class GenericMaterialsModel(BaseMaterialsModel): class GenericMaterialsModel(BaseMaterialsModel):
@ -38,6 +40,24 @@ class GenericMaterialsModel(BaseMaterialsModel):
self.setItems([]) self.setItems([])
return return
#special case only for Ultimaker printers, filter the generic list
printer_name = global_stack.getMetaDataEntry("name", "empty")
filter_ultimaker_printers = False
if printer_name and printer_name[:9] == "Ultimaker":
filter_ultimaker_printers = True
# Special case, Ultimaker generic list also should be filtered
if filter_ultimaker_printers is False:
item_list = self._getGenericProfiles(available_material_dict)
else:
item_list = self._getUltimakerGenericProfiles(available_material_dict)
# Sort the item list by material name alphabetically
item_list = sorted(item_list, key = lambda d: d["name"].upper())
self.setItems(item_list)
def _getGenericProfiles(self, available_material_dict):
item_list = [] item_list = []
for root_material_id, container_node in available_material_dict.items(): for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata metadata = container_node.metadata
@ -55,7 +75,42 @@ class GenericMaterialsModel(BaseMaterialsModel):
} }
item_list.append(item) item_list.append(item)
# Sort the item list by material name alphabetically return item_list
item_list = sorted(item_list, key = lambda d: d["name"].upper())
self.setItems(item_list) ## The method filters available materials by name. If material is not defined for Ultimaker printers
# then it will be removed
# \available_material_dict \type{dictionary}
# \return The filtered list
def _getUltimakerGenericProfiles(self, available_material_dict: Dict[str, MaterialNode]):
generic_item_list = []
ultimaker_item_list = []
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
is_ultimaker_brand = False
brand_name = metadata["brand"].lower()
if brand_name != "generic":
if brand_name == 'ultimaker':
is_ultimaker_brand = True
else:
continue
item = {"root_material_id": root_material_id,
"id": metadata["id"],
"name": metadata["name"],
"brand": metadata["brand"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"container_node": container_node
}
if is_ultimaker_brand:
ultimaker_item_list.append(item['material'])
else:
generic_item_list.append(item)
# If material is not in ultimaker list then remove it
item_list = [material for material in generic_item_list if material['material'] in ultimaker_item_list]
return item_list

View File

@ -8,6 +8,8 @@ from UM.Application import Application
from UM.Extension import Extension from UM.Extension import Extension
from UM.Logger import Logger 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 os.path import os.path
import pkgutil import pkgutil
import sys import sys
@ -35,6 +37,8 @@ class PostProcessingPlugin(QObject, Extension):
self._selected_script_index = -1 self._selected_script_index = -1
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) 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.
selectedIndexChanged = pyqtSignal() selectedIndexChanged = pyqtSignal()
@pyqtProperty("QVariant", notify = selectedIndexChanged) @pyqtProperty("QVariant", notify = selectedIndexChanged)
@ -110,10 +114,32 @@ class PostProcessingPlugin(QObject, Extension):
self.selectedIndexChanged.emit() # Ensure that settings are updated self.selectedIndexChanged.emit() # Ensure that settings are updated
self._propertyChanged() self._propertyChanged()
## 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.
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.
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadScripts(path)
## Load all scripts from provided path. ## Load all scripts from provided path.
# This should probably only be done on init. # This should probably only be done on init.
# \param path Path to check for scripts. # \param path Path to check for scripts.
def loadAllScripts(self, path): def loadScripts(self, path):
## Load all scripts in the scripts folders
scripts = pkgutil.iter_modules(path = [path]) scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts: for loader, script_name, ispkg in scripts:
# Iterate over all scripts. # Iterate over all scripts.
@ -122,7 +148,7 @@ class PostProcessingPlugin(QObject, Extension):
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) 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) loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script) spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
loaded_class = getattr(loaded_script, script_name) loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class() temp_object = loaded_class()
@ -142,7 +168,6 @@ class PostProcessingPlugin(QObject, Extension):
Logger.log("e", "Script %s.py has no implemented settings", script_name) Logger.log("e", "Script %s.py has no implemented settings", script_name)
except Exception as e: except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e))) Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
self.loadedScriptListChanged.emit()
loadedScriptListChanged = pyqtSignal() loadedScriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = loadedScriptListChanged) @pyqtProperty("QVariantList", notify = loadedScriptListChanged)
@ -168,24 +193,67 @@ class PostProcessingPlugin(QObject, Extension):
self.scriptListChanged.emit() self.scriptListChanged.emit()
self._propertyChanged() self._propertyChanged()
## When the global container stack is changed, swap out the list of active
# scripts.
def _onGlobalContainerStackChanged(self):
self.loadAllScripts()
new_stack = Application.getInstance().getGlobalContainerStack()
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.
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).
continue
script_str = script_str.replace("\\n", "\n").replace("\\\\", "\\") #Unescape escape sequences.
script_parser = configparser.ConfigParser(interpolation = None)
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.
continue
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)
self._script_list.append(new_script)
self.setSelectedScriptIndex(0)
self.scriptListChanged.emit()
@pyqtSlot()
def writeScriptsToStack(self):
script_list_strs = []
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.
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.
parser.write(serialized)
serialized.seek(0)
script_str = serialized.read()
script_str = script_str.replace("\\", "\\\\").replace("\n", "\\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.
global_stack = Application.getInstance().getGlobalContainerStack()
if "post_processing_scripts" not in global_stack.getMetaData():
global_stack.addMetaDataEntry("post_processing_scripts", "")
Application.getInstance().getGlobalContainerStack().setMetaDataEntry("post_processing_scripts", script_list_strs)
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self): def _createView(self):
Logger.log("d", "Creating post processing plugin view.") Logger.log("d", "Creating post processing plugin view.")
## Load all scripts in the scripts folders self.loadAllScripts()
# 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)]:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadAllScripts(path)
# Create the plugin dialog component # Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")

View File

@ -21,6 +21,14 @@ UM.Dialog
minimumWidth: 400 * screenScaleFactor; minimumWidth: 400 * screenScaleFactor;
minimumHeight: 250 * screenScaleFactor; minimumHeight: 250 * screenScaleFactor;
onVisibleChanged:
{
if(!visible) //Whenever the window is closed (either via the "Close" button or the X on the window frame), we want to update it in the stack.
{
manager.writeScriptsToStack();
}
}
Item Item
{ {
UM.I18nCatalog{id: catalog; name:"cura"} UM.I18nCatalog{id: catalog; name:"cura"}