# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2020 sliptonic * # * * # * 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 * # * * # *************************************************************************** from PySide import QtCore, QtGui import FreeCAD import FreeCADGui import Path import Path.Base.Gui.IconViewProvider as PathIconViewProvider import Path.Base.Gui.PropertyEditor as PathPropertyEditor import Path.Base.PropertyBag as PathPropertyBag import Path.Base.Util as PathUtil import re __title__ = "Property Bag Editor" __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecad.org" __doc__ = "Task panel editor for a PropertyBag" if False: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) translate = FreeCAD.Qt.translate class ViewProvider(object): """ViewProvider for a PropertyBag. It's sole job is to provide an icon and invoke the TaskPanel on edit.""" def __init__(self, vobj, name): Path.Log.track(name) vobj.Proxy = self self.icon = name # mode = 2 self.obj = None self.vobj = None def attach(self, vobj): Path.Log.track() self.vobj = vobj self.obj = vobj.Object def getIcon(self): return ":/icons/Path-SetupSheet.svg" def dumps(self): return None def loads(self, state): return None def getDisplayMode(self, mode): return "Default" def setEdit(self, vobj, mode=0): Path.Log.track() taskPanel = TaskPanel(vobj) FreeCADGui.Control.closeDialog() FreeCADGui.Control.showDialog(taskPanel) taskPanel.setupUi() return True def unsetEdit(self, vobj, mode): FreeCADGui.Control.closeDialog() return def claimChildren(self): return [] def doubleClicked(self, vobj): self.setEdit(vobj) class Delegate(QtGui.QStyledItemDelegate): RoleObject = QtCore.Qt.UserRole + 1 RoleProperty = QtCore.Qt.UserRole + 2 RoleEditor = QtCore.Qt.UserRole + 3 # def paint(self, painter, option, index): # #Path.Log.track(index.column(), type(option)) def createEditor(self, parent, option, index): editor = PathPropertyEditor.Editor( index.data(self.RoleObject), index.data(self.RoleProperty) ) index.model().setData(index, editor, self.RoleEditor) return editor.widget(parent) def setEditorData(self, widget, index): Path.Log.track(index.row(), index.column()) index.data(self.RoleEditor).setEditorData(widget) def setModelData(self, widget, model, index): Path.Log.track(index.row(), index.column()) editor = index.data(self.RoleEditor) editor.setModelData(widget) index.model().setData(index, editor.displayString(), QtCore.Qt.DisplayRole) def updateEditorGeometry(self, widget, option, index): widget.setGeometry(option.rect) class PropertyCreate(object): def __init__(self, obj, grp, typ, another): self.obj = obj self.form = FreeCADGui.PySideUic.loadUi(":panels/PropertyCreate.ui") obj.Proxy.refreshCustomPropertyGroups() groups = obj.CustomPropertyGroups if not isinstance(groups, (list, tuple)): groups = [groups] for g in sorted(groups): self.form.propertyGroup.addItem(g) if grp: self.form.propertyGroup.setCurrentText(grp) for t in sorted(PathPropertyBag.SupportedPropertyType): self.form.propertyType.addItem(t) if PathPropertyBag.SupportedPropertyType[t] == typ: typ = t if typ: self.form.propertyType.setCurrentText(typ) else: self.form.propertyType.setCurrentText("String") self.form.createAnother.setChecked(another) self.form.propertyGroup.currentTextChanged.connect(self.updateUI) self.form.propertyGroup.currentIndexChanged.connect(self.updateUI) self.form.propertyName.textChanged.connect(self.updateUI) self.form.propertyType.currentIndexChanged.connect(self.updateUI) self.form.propertyEnum.textChanged.connect(self.updateUI) def updateUI(self): typeSet = True if self.propertyIsEnumeration(): self.form.labelEnum.setEnabled(True) self.form.propertyEnum.setEnabled(True) typeSet = self.form.propertyEnum.text().strip() != "" else: self.form.labelEnum.setEnabled(False) self.form.propertyEnum.setEnabled(False) if self.form.propertyEnum.text().strip(): self.form.propertyEnum.setText("") ok = self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok) if not re.match("^[A-Za-z0-9_]*$", self.form.propertyName.text()): typeSet = False if typeSet and self.propertyGroup(): ok.setEnabled(True) else: ok.setEnabled(False) def propertyName(self): return self.form.propertyName.text().strip() def propertyGroup(self): return self.form.propertyGroup.currentText().strip() def propertyType(self): return PathPropertyBag.SupportedPropertyType[self.form.propertyType.currentText()].strip() def propertyInfo(self): return self.form.propertyInfo.toPlainText().strip() def createAnother(self): return self.form.createAnother.isChecked() def propertyEnumerations(self): return [s.strip() for s in self.form.propertyEnum.text().strip().split(",")] def propertyIsEnumeration(self): return self.propertyType() == "App::PropertyEnumeration" def exec_(self, name): if name: # property exists - this is an edit operation self.form.propertyName.setText(name) if self.propertyIsEnumeration(): self.form.propertyEnum.setText(",".join(self.obj.getEnumerationsOfProperty(name))) self.form.propertyInfo.setText(self.obj.getDocumentationOfProperty(name)) self.form.labelName.setEnabled(False) self.form.propertyName.setEnabled(False) self.form.labelType.setEnabled(False) self.form.propertyType.setEnabled(False) self.form.createAnother.setEnabled(False) else: self.form.propertyName.setText("") self.form.propertyInfo.setText("") self.form.propertyEnum.setText("") # self.form.propertyName.setFocus() self.updateUI() return self.form.exec_() class TaskPanel(object): ColumnName = 0 # ColumnType = 1 ColumnVal = 1 # TableHeaders = ['Property', 'Type', 'Value'] TableHeaders = ["Property", "Value"] def __init__(self, vobj): self.obj = vobj.Object self.props = sorted(self.obj.Proxy.getCustomProperties()) self.form = FreeCADGui.PySideUic.loadUi(":panels/PropertyBag.ui") # initialized later self.model = None self.delegate = None FreeCAD.ActiveDocument.openTransaction("Edit PropertyBag") def updateData(self, topLeft, bottomRight): pass def _setupProperty(self, i, name): if name not in self.obj.PropertiesList: Path.Log.warning(f"Property '{name}' not found in object {self.obj.Name}") return prop_type_id = self.obj.getTypeIdOfProperty(name) try: typ = PathPropertyBag.getPropertyTypeName(prop_type_id) except IndexError: Path.Log.error( f"Unknown property type id '{prop_type_id}' for property '{name}' in object {self.obj.Name}" ) return val = PathUtil.getPropertyValueString(self.obj, name) info = self.obj.getDocumentationOfProperty(name) self.model.setData(self.model.index(i, self.ColumnName), name, QtCore.Qt.EditRole) # self.model.setData(self.model.index(i, self.ColumnType), typ, QtCore.Qt.EditRole) self.model.setData(self.model.index(i, self.ColumnVal), self.obj, Delegate.RoleObject) self.model.setData(self.model.index(i, self.ColumnVal), name, Delegate.RoleProperty) self.model.setData(self.model.index(i, self.ColumnVal), val, QtCore.Qt.DisplayRole) self.model.setData(self.model.index(i, self.ColumnName), typ, QtCore.Qt.ToolTipRole) # self.model.setData(self.model.index(i, self.ColumnType), info, QtCore.Qt.ToolTipRole) self.model.setData(self.model.index(i, self.ColumnVal), info, QtCore.Qt.ToolTipRole) self.model.item(i, self.ColumnName).setEditable(False) # self.model.item(i, self.ColumnType).setEditable(False) def setupUi(self): Path.Log.track() self.delegate = Delegate(self.form) self.model = QtGui.QStandardItemModel(len(self.props), len(self.TableHeaders), self.form) self.model.setHorizontalHeaderLabels(self.TableHeaders) for i, name in enumerate(self.props): self._setupProperty(i, name) self.form.table.setModel(self.model) self.form.table.setItemDelegateForColumn(self.ColumnVal, self.delegate) self.form.table.resizeColumnsToContents() self.model.dataChanged.connect(self.updateData) self.form.table.selectionModel().selectionChanged.connect(self.propertySelected) self.form.add.clicked.connect(self.propertyAdd) self.form.remove.clicked.connect(self.propertyRemove) self.form.modify.clicked.connect(self.propertyModify) self.form.table.doubleClicked.connect(self.propertyModifyIndex) self.propertySelected([]) def accept(self): FreeCAD.ActiveDocument.commitTransaction() FreeCADGui.ActiveDocument.resetEdit() FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() def reject(self): FreeCAD.ActiveDocument.abortTransaction() FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() def propertySelected(self, selection): Path.Log.track() if selection: self.form.modify.setEnabled(True) self.form.remove.setEnabled(True) else: self.form.modify.setEnabled(False) self.form.remove.setEnabled(False) def addCustomProperty(self, obj, dialog): name = dialog.propertyName() typ = dialog.propertyType() grp = dialog.propertyGroup() info = dialog.propertyInfo() if name: propname = self.obj.Proxy.addCustomProperty(typ, name, grp, info) if dialog.propertyIsEnumeration(): setattr(self.obj, name, dialog.propertyEnumerations()) return (propname, info) return (None, None) def propertyAdd(self): Path.Log.track() more = False grp = None typ = None while True: dialog = PropertyCreate(self.obj, grp, typ, more) if dialog.exec_(None): # if we block signals the view doesn't get updated, surprise, surprise # self.model.blockSignals(True) name, info = self.addCustomProperty(self.obj, dialog) if name: index = 0 for i in range(self.model.rowCount()): index = i if ( self.model.item(i, self.ColumnName).data(QtCore.Qt.EditRole) > dialog.propertyName() ): break self.model.insertRows(index, 1) self._setupProperty(index, name) self.form.table.selectionModel().setCurrentIndex( self.model.index(index, 0), QtCore.QItemSelectionModel.Rows ) # self.model.blockSignals(False) more = dialog.createAnother() else: more = False else: more = False if not more: break def propertyModifyIndex(self, index): Path.Log.track(index.row(), index.column()) row = index.row() obj = self.model.item(row, self.ColumnVal).data(Delegate.RoleObject) prop = self.model.item(row, self.ColumnVal).data(Delegate.RoleProperty) grp = obj.getGroupOfProperty(prop) typ = obj.getTypeIdOfProperty(prop) dialog = PropertyCreate(self.obj, grp, typ, False) if dialog.exec_(prop): val = getattr(obj, prop) obj.removeProperty(prop) name, info = self.addCustomProperty(self.obj, dialog) try: setattr(obj, prop, val) except Exception: # this can happen if the old enumeration value doesn't exist anymore pass newVal = PathUtil.getPropertyValueString(obj, prop) self.model.setData(self.model.index(row, self.ColumnVal), newVal, QtCore.Qt.DisplayRole) # self.model.setData(self.model.index(row, self.ColumnType), info, QtCore.Qt.ToolTipRole) self.model.setData(self.model.index(row, self.ColumnVal), info, QtCore.Qt.ToolTipRole) def propertyModify(self): Path.Log.track() rows = [] for index in self.form.table.selectionModel().selectedIndexes(): row = index.row() if row in rows: continue rows.append(row) self.propertyModifyIndex(index) def propertyRemove(self): Path.Log.track() # first find all rows which need to be removed rows = [] for index in self.form.table.selectionModel().selectedIndexes(): if not index.row() in rows: rows.append(index.row()) # then remove them in reverse order so the indexes of the remaining rows # to delete are still valid for row in reversed(sorted(rows)): self.obj.removeProperty(self.model.item(row).data(QtCore.Qt.EditRole)) self.model.removeRow(row) def Create(name="PropertyBag"): """Create(name = 'PropertyBag') ... creates a new setup sheet""" FreeCAD.ActiveDocument.openTransaction("Create PropertyBag") pcont = PathPropertyBag.Create(name) PathIconViewProvider.Attach(pcont.ViewObject, name) return pcont PathIconViewProvider.RegisterViewProvider("PropertyBag", ViewProvider) class PropertyBagCreateCommand(object): """Command to create a property container object""" def __init__(self): pass def GetResources(self): return { "MenuText": translate("CAM_PropertyBag", "Property Bag"), "ToolTip": translate( "CAM_PropertyBag", "Creates an object which can be used to store reference properties", ), } def IsActive(self): return not FreeCAD.ActiveDocument is None def Activated(self): sel = FreeCADGui.Selection.getSelectionEx() obj = Create() body = None if sel: if "PartDesign::Body" == sel[0].Object.TypeId: body = sel[0].Object elif hasattr(sel[0].Object, "getParentGeoFeatureGroup"): body = sel[0].Object.getParentGeoFeatureGroup() if body: obj.Label = "Attributes" group = body.Group group.append(obj) body.Group = group if FreeCAD.GuiUp: FreeCADGui.addCommand("CAM_PropertyBag", PropertyBagCreateCommand()) FreeCAD.Console.PrintLog("Loading PathPropertyBagGui… done\n")