# *************************************************************************** # * Copyright (c) 2013 Yorik van Havre * # * Copyright (c) 2019 Bernd Hahnebach * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** __title__ = "FreeCAD material editor" __author__ = "Yorik van Havre, Bernd Hahnebach" __url__ = "http://www.freecadweb.org" import os import sys from PySide import QtCore, QtGui # from PySide import QtUiTools, QtSvg import FreeCAD import FreeCADGui if sys.version_info.major >= 3: unicode = str class MaterialEditor: def __init__(self, obj=None, prop=None, material=None): """Initializes, optionally with an object name and a material property name to edit, or directly with a material dictionary.""" self.obj = obj self.prop = prop self.material = material self.customprops = [] self.internalprops = [] self.groups = [] self.directory = FreeCAD.getResourceDir() + "Mod/Material" # load the UI file from the same directory as this script self.widget = FreeCADGui.PySideUic.loadUi( os.path.dirname(__file__) + os.sep + "materials-editor.ui" ) # additional UI fixes and tweaks widget = self.widget buttonURL = widget.ButtonURL buttonDeleteProperty = widget.ButtonDeleteProperty buttonAddProperty = widget.ButtonAddProperty standardButtons = widget.standardButtons buttonOpen = widget.ButtonOpen buttonSave = widget.ButtonSave comboMaterial = widget.ComboMaterial treeView = widget.treeView # temporarily hide preview fields, as they are not used yet # TODO : implement previews widget.PreviewGroup.hide() buttonURL.setIcon(QtGui.QIcon(":/icons/internet-web-browser.svg")) buttonDeleteProperty.setEnabled(False) standardButtons.button(QtGui.QDialogButtonBox.Ok).setAutoDefault(False) standardButtons.button(QtGui.QDialogButtonBox.Cancel).setAutoDefault(False) self.updateCards() # TODO allow to enter a custom property by pressing Enter in the lineedit # currently closes the dialog standardButtons.rejected.connect(self.reject) standardButtons.accepted.connect(self.accept) buttonOpen.clicked.connect(self.openfile) buttonSave.clicked.connect(self.savefile) buttonURL.clicked.connect(self.openProductURL) comboMaterial.currentIndexChanged[str].connect(self.updateContents) buttonAddProperty.clicked.connect(self.addCustomProperty) buttonDeleteProperty.clicked.connect(self.deleteCustomProperty) treeView.clicked.connect(self.checkDeletable) model = QtGui.QStandardItemModel() treeView.setModel(model) treeView.setUniformRowHeights(True) treeView.setItemDelegate(MaterialsDelegate()) # update the editor with the contents of the property, if we have one d = None if self.prop and self.obj: d = FreeCAD.ActiveDocument.getObject(self.obj).getPropertyByName(self.prop) elif self.material: d = self.material self.implementModel() if d: self.updateContents(d) def implementModel(self): '''implements the model with the material attribute structure.''' widget = self.widget treeView = widget.treeView model = treeView.model() model.setHorizontalHeaderLabels(["Property", "Value", "Type"]) treeView.setColumnWidth(0, 250) treeView.setColumnWidth(1, 250) treeView.setColumnHidden(2, True) from Material import getMaterialAttributeStructure tree = getMaterialAttributeStructure(True) MatPropDict = tree.getroot() for group in MatPropDict.getchildren(): gg = group.attrib['Name'] top = QtGui.QStandardItem(gg) model.appendRow([top]) self.groups.append(gg) for proper in group.getchildren(): properDict = proper.attrib pp = properDict['Name'] item = QtGui.QStandardItem(pp) self.internalprops.append(pp) it = QtGui.QStandardItem() tt = properDict['Type'] itType = QtGui.QStandardItem(tt) top.appendRow([item, it, itType]) top.sortChildren(0) treeView.expandAll() def updateContents(self, data): '''updates the contents of the editor with the given data, can be: - a dictionary, if the editor was called with data - a string, the name of a card, if material is changed in editors combo box the material property keys where added to the editor already not known material property keys will be added to the user defined group''' # print type(data) if isinstance(data, dict): # a standard material property dict is provided model = self.widget.treeView.model() root = model.invisibleRootItem() for gg in range(root.rowCount() - 1): group = root.child(gg, 0) for pp in range(group.rowCount()): item = group.child(pp, 0) it = group.child(pp, 1) kk = self.collapseKey(item.text()) try: value = data[kk] it.setText(value) del data[kk] except KeyError: it.setText("") userGroup = root.child(gg + 1, 0) userGroup.setRowCount(0) self.customprops = [] for k, i in data.items(): k = self.expandKey(k) item = QtGui.QStandardItem(k) it = QtGui.QStandardItem(i) userGroup.appendRow([item, it]) self.customprops.append(k) elif isinstance(data, unicode): # a card name is provided, search card, read material data and call # this def once more with std material property dict k = str(data) if k: if k in self.cards: from importFCMat import read d = read(self.cards[k]) if d: self.updateContents(d) def getMaterialResources(self): self.fem_prefs = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/Material/Resources" ) use_built_in_materials = self.fem_prefs.GetBool("UseBuiltInMaterials", True) use_mat_from_config_dir = self.fem_prefs.GetBool("UseMaterialsFromConfigDir", True) use_mat_from_custom_dir = self.fem_prefs.GetBool("UseMaterialsFromCustomDir", True) if use_mat_from_custom_dir: custom_mat_dir = self.fem_prefs.GetString("CustomMaterialsDir", "") # later found cards with same name will override cards # FreeCAD returns paths with / at the end, thus not os.sep is needed on first + self.resources = [] if use_built_in_materials: res_dir = FreeCAD.getResourceDir() self.resources.append( res_dir + "Mod" + os.sep + "Material" + os.sep + "StandardMaterial" ) if use_mat_from_config_dir: self.resources.append(FreeCAD.ConfigGet("UserAppData") + "Material") if use_mat_from_custom_dir: custom_mat_dir = self.fem_prefs.GetString("CustomMaterialsDir", "") if os.path.exists(custom_mat_dir): self.resources.append(custom_mat_dir) self.outputResources() def outputResources(self): print('locations to look for material cards:') for path in self.resources: print(' ' + path) print('\n') def outputCards(self): print('material cards:') for card in sorted(self.cards.keys()): print(' ' + card + ': ' + self.cards[card]) print('\n') def updateCards(self): '''updates the contents of the materials combo with existing material cards''' self.getMaterialResources() self.cards = {} for p in self.resources: if os.path.exists(p): for f in sorted(os.listdir(p)): b, e = os.path.splitext(f) if e.upper() == ".FCMAT": self.cards[b] = p + os.sep + f # self.outputCards() if self.cards: self.widget.ComboMaterial.clear() self.widget.ComboMaterial.addItem("") # add a blank item first for card in sorted(self.cards.keys()): self.widget.ComboMaterial.addItem(card) # all keys in self.cards are unicode def openProductURL(self): '''opens the contents of the ProductURL field in an external browser.''' model = self.widget.treeView.model() item = model.findItems(translate("Material", "Product URL"), QtCore.Qt.MatchRecursive, 0)[0] group = item.parent() it = group.child(item.row(), 1) url = it.text() if url: QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode)) def accept(self): "" QtGui.QDialog.accept(self.widget) def reject(self): "" QtGui.QDialog.reject(self.widget) def expandKey(self, key): "adds spaces before caps in a KeyName" nk = "" for ln in key: if ln.isupper(): if nk: # this allows for series of caps, such as ProductURL if not nk[-1].isupper(): nk += " " nk += ln return nk def collapseKey(self, key): "removes the spaces in a Key Name" nk = "" for ln in key: if ln != " ": nk += ln return nk def addCustomProperty(self, key=None, value=None): "Adds a custom property to the editor, optionally with a value." if not key: key = self.widget.EditProperty.text() if key: model = self.widget.treeView.model() item = model.findItems(key, QtCore.Qt.MatchRecursive, 0) if not item: top = model.findItems(translate("Material", "User defined"), QtCore.Qt.MatchExactly, 0)[0] item = QtGui.QStandardItem(key) it = QtGui.QStandardItem(value) top.appendRow([item, it]) self.customprops.append(key) def deleteCustomProperty(self, key=None): '''Deletes a custom property from the editor, or deletes the value of an internal property.''' widget = self.widget treeView = widget.treeView model = treeView.model() buttonDeleteProperty = widget.ButtonDeleteProperty if not key: index = treeView.selectedIndexes()[0] item = model.itemFromIndex(index) key = item.text() if key: item = model.findItems(key, QtCore.Qt.MatchRecursive, 0) if item: index = model.indexFromItem(item[0]) topIndex = index.parent() top = model.itemFromIndex(topIndex) row = item[0].row() if key in self.customprops: top.takeRow(row) self.customprops.remove(key) buttonDeleteProperty.setProperty("text", "Delete property") elif key in self.internalprops: it = top.child(row, 1) it.setText("") buttonDeleteProperty.setProperty("text", "Delete value") buttonDeleteProperty.setEnabled(False) def checkDeletable(self, index): '''Checks if the current item is a custom or an internal property, and enable the delete property or delete value button.''' widget = self.widget buttonDeleteProperty = widget.ButtonDeleteProperty treeView = widget.treeView model = treeView.model() ind = treeView.selectedIndexes()[0] item = model.itemFromIndex(ind) text = item.text() if text in self.customprops: buttonDeleteProperty.setEnabled(True) buttonDeleteProperty.setProperty("text", "Delete property") elif text in self.internalprops: indParent = ind.parent() group = model.itemFromIndex(indParent) row = item.row() it = group.child(row, 1) buttonDeleteProperty.setProperty("text", "Delete value") if it.text(): buttonDeleteProperty.setEnabled(True) else: buttonDeleteProperty.setEnabled(False) else: buttonDeleteProperty.setEnabled(False) buttonDeleteProperty.setProperty("text", "Delete property") def getDict(self): "returns a dictionary from the contents of the editor." model = self.widget.treeView.model() root = model.invisibleRootItem() d = {} for gg in range(root.rowCount()): group = root.child(gg) for row in range(group.rowCount()): kk = group.child(row, 0).text() ii = group.child(row, 1).text() # TODO the following should be translated back to english # since text(0) could be translated matkey = self.collapseKey(str(kk)) matvalue = unicode(ii) if matvalue or (matkey == 'Name'): # use only keys which are not empty and the name even if empty d[matkey] = matvalue # self.outputDict(d) return d def outputDict(self, d): print('MaterialEditor dictionary') for param in d: print(' {} : {}'.format(param, d[param])) ''' def setTexture(self, pattern): "displays a texture preview if needed" self.widget.PreviewVector.hide() if pattern: try: import DrawingPatterns except: print("DrawingPatterns not found") else: pattern = DrawingPatterns.buildFileSwatch(pattern, size=96, png=True) if pattern: self.widget.PreviewVector.setPixmap(QtGui.QPixmap(pattern)) self.widget.PreviewVector.show() ''' def openfile(self): "Opens a FCMat file" filetuple = QtGui.QFileDialog.getOpenFileName( QtGui.QApplication.activeWindow(), 'Open FreeCAD Material file', self.directory, '*.FCMat' ) # a tuple of two empty strings returns True, so use the filename directly filename = filetuple[0] if filename: from importFCMat import read self.directory = os.path.dirname(filename) d = read(filename) if d: self.updateContents(d) def savefile(self): "Saves a FCMat file." model = self.widget.treeView.model() item = model.findItems(translate("Material", "Name"), QtCore.Qt.MatchRecursive, 0)[0] group = item.parent() it = group.child(item.row(), 1) name = it.text() if sys.version_info.major < 3: if isinstance(name, unicode): name = name.encode("utf8") if not name: name = "Material" filetuple = QtGui.QFileDialog.getSaveFileName( QtGui.QApplication.activeWindow(), 'Save FreeCAD Material file', self.directory + '/' + name + '.FCMat', '*.FCMat' ) # a tuple of two empty strings returns True, so use the filename directly filename = filetuple[0] if filename: self.directory = os.path.dirname(filename) d = self.getDict() # self.outputDict(d) if d: from importFCMat import write write(filename, d) self.updateCards() def show(self): return self.widget.show() def exec_(self): return self.widget.exec_() class MaterialsDelegate(QtGui.QStyledItemDelegate): '''provides display and editing facilities for data items from a model.''' def __init__(self): "" super(MaterialsDelegate, self).__init__() def createEditor(self, parent, option, index): '''returns the widget used to change data from the model.''' model = index.model() column = index.column() item = model.itemFromIndex(index) group = item.parent() if not group: return if column == 1: row = index.row() PP = group.child(row, 0) matproperty = PP.text().replace(" ", "") # remove spaces TT = group.child(row, 2) if TT: Type = TT.text() else: Type = "String" VV = group.child(row, 1) Value = VV.text() editor = matProperWidget(parent, matproperty, Type, Value) elif column == 0: if group.text() == "User defined": editor = matProperWidget(parent) else: return else: return return editor def setEditorData(self, editor, index): '''provides the widget with data to manipulate.''' Type = editor.property('Type') model = index.model() item = model.itemFromIndex(index) if Type == "Color": color = editor.property('color') color = color.getRgb() item.setText(str(color)) elif Type == "File": lineEdit = editor.children()[1] item.setText(lineEdit.text()) else: super(MaterialsDelegate, self).setEditorData(editor, index) ui = FreeCADGui.UiLoader() def matProperWidget(parent=None, matproperty=None, Type="String", Value=None, minimum=None, maximum=None, stepsize=None, precision=None): '''customs widgets for the material stuff.''' if Type == "String": widget = ui.createWidget("Gui::PrefLineEdit") elif Type == "URL": widget = ui.createWidget("Gui::PrefLineEdit") elif Type == "File": widget = ui.createWidget("Gui::FileChooser") if Value: lineEdit = widget.children()[1] lineEdit.setText(Value) elif Type == "Quantity": widget = ui.createWidget("Gui::InputField") if hasattr(FreeCAD.Units, matproperty): unit = getattr(FreeCAD.Units, matproperty) quantity = FreeCAD.Units.Quantity(1, unit) widget.setProperty('unit', quantity.getUserPreferred()[2]) else: FreeCAD.Console.PrintError('Not known unit for property: {}\n'.format(matproperty)) elif Type == "Integer": widget = ui.createWidget("Gui::UIntSpinBox") elif Type == "Float": widget = ui.createWidget("Gui::PrefDoubleSpinBox") elif Type == "Enumerator": widget = ui.createWidget("Gui::PrefComboBox") elif Type == "Boolean": widget = ui.createWidget("Gui::PrefComboBox") widget.insertItems(0, ['', 'False', 'True']) elif Type == "Vector": widget = ui.createWidget("Gui::PrefLineEdit") elif Type == "Color": widget = ui.createWidget("Gui::PrefColorButton") if Value: value = string2tuple(Value) color = QtGui.QColor() color.setRgb(value[0], value[1], value[2], value[3]) widget.setProperty('color', color) else: widget = QtGui.QLineEdit() if minimum is not None: widget.setProperty('minimum', minimum) if maximum is not None: widget.setProperty('maximum', maximum) if stepsize is not None: widget.setProperty('stepsize', stepsize) if precision is not None: widget.setProperty('precision', precision) widget.setProperty('Type', Type) widget.setParent(parent) return widget def string2tuple(string): "provisionally" value = string[1:-1] value = value.split(',') value = [int(v) for v in value] value = tuple(value) return value def translate(context, text): "translates text" return text # TODO use Qt translation mechanism here def openEditor(obj=None, prop=None): """openEditor([obj,prop]): opens the editor, optionally with an object name and material property name to edit""" editor = MaterialEditor(obj, prop) editor.exec_() def editMaterial(material): """editMaterial(material): opens the editor to edit the contents of the given material dictionary. Returns the modified material dictionary.""" # if the material editor is opened with this def the combo box with the card name is empty # this makes sense ... # because the editor was not opened with a card but with material dictionary instead # TODO: add some text in combo box, may be "custom material data" or "user material data" # TODO: all card could be checked if one fits exact ALL provided data # than this card name could be displayed editor = MaterialEditor(material=material) result = editor.exec_() if result: return editor.getDict() else: return material ''' # some examples how to open the material editor in Python: import MaterialEditor MaterialEditor.openEditor() doc = FreeCAD.open( FreeCAD.ConfigGet("AppHomePath") + 'data/examples/FemCalculixCantilever3D.FCStd' ) import MaterialEditor MaterialEditor.openEditor('SolidMaterial', 'Material') import MaterialEditor MaterialEditor.editMaterial({ 'Density': '1234.0 kg/m^3', 'Name': 'My-Material-Data', 'PoissonRatio': '0.66', 'YoungsModulus': '123456 MPa' }) import MaterialEditor MaterialEditor.editMaterial('ABS') '''