diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 444be1ecd4..1e1c4e305c 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -96,6 +96,10 @@ SET(PathScripts_SRCS
PathScripts/PathProfileFaces.py
PathScripts/PathProfileFacesGui.py
PathScripts/PathProfileGui.py
+ PathScripts/PathProperty.py
+ PathScripts/PathPropertyBag.py
+ PathScripts/PathPropertyBagGui.py
+ PathScripts/PathPropertyEditor.py
PathScripts/PathSanity.py
PathScripts/PathSelection.py
PathScripts/PathSetupSheet.py
@@ -198,6 +202,7 @@ SET(PathTests_SRCS
PathTests/TestPathOpTools.py
PathTests/TestPathPost.py
PathTests/TestPathPreferences.py
+ PathTests/TestPathPropertyBag.py
PathTests/TestPathSetupSheet.py
PathTests/TestPathStock.py
PathTests/TestPathThreadMilling.py
@@ -208,6 +213,9 @@ SET(PathTests_SRCS
PathTests/TestPathUtil.py
PathTests/TestPathVcarve.py
PathTests/TestPathVoronoi.py
+ PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb
+ PathTests/Tools/Library/test-path-tool-bit-library-00.fctl
+ PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd
PathTests/boxtest.fcstd
PathTests/test_centroid_00.ngc
PathTests/test_geomop.fcstd
@@ -279,6 +287,14 @@ INSTALL(
Mod/Path/PathTests
)
+INSTALL(
+ DIRECTORY
+ PathTests/Tools
+ DESTINATION
+ Mod/Path/PathTests
+)
+
+
INSTALL(
FILES
${PathScripts_post_SRCS}
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index c99246c667..832f74f868 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -121,6 +121,8 @@
panels/PageOpVcarveEdit.ui
panels/PathEdit.ui
panels/PointEdit.ui
+ panels/PropertyBag.ui
+ panels/PropertyCreate.ui
panels/SetupGlobal.ui
panels/SetupOp.ui
panels/ToolBitEditor.ui
diff --git a/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui b/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui
new file mode 100644
index 0000000000..ac9af5c8d5
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PropertyBag.ui
@@ -0,0 +1,78 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 552
+ 651
+
+
+
+ Property Bag
+
+
+ -
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ QAbstractItemView::AllEditTriggers
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
-
+
+
+ Remove
+
+
+
+ -
+
+
+ Modify...
+
+
+
+ -
+
+
+ Add...
+
+
+
+
+
+
+
+
+
+ table
+ add
+ remove
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui b/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui
new file mode 100644
index 0000000000..c65c02ea82
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PropertyCreate.ui
@@ -0,0 +1,182 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 480
+ 452
+
+
+
+ Create Property
+
+
+ -
+
+
+ Name
+
+
+
+ -
+
+
+ <html><head/><body><p>Name of property.</p></body></html>
+
+
+
+ -
+
+
+ <html><head/><body><p>The category group the property belongs to.</p></body></html>
+
+
+ true
+
+
+
+ -
+
+
+ Group
+
+
+
+ -
+
+
+ <html><head/><body><p>The type of the property value.</p></body></html>
+
+
+
+ -
+
+
+ Type
+
+
+
+ -
+
+
+ val1,val2,val3,...
+
+
+
+ -
+
+
+ <html><head/><body><p>ToolTip to be displayed when user hovers mouse over property.</p></body></html>
+
+
+ true
+
+
+
+ -
+
+
+ Enums
+
+
+
+ -
+
+
+ ToolTip
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ <html><head/><body><p>Check if you want to create several properties in a batch.</p></body></html>
+
+
+ Create another
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+
+ propertyName
+ propertyGroup
+ propertyType
+ propertyEnum
+ propertyInfo
+ createAnother
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui
index 1342da5649..d77a523a12 100644
--- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui
+++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui
@@ -6,7 +6,7 @@
0
0
- 401
+ 489
715
@@ -17,7 +17,7 @@
-
-
+
0
0
@@ -32,6 +32,12 @@
-
+
+
+ 0
+ 0
+
+
Tool Bit
@@ -65,6 +71,12 @@
-
+
+
+ 0
+ 0
+
+
0
@@ -83,6 +95,9 @@
<html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html>
+
+ path
+
-
@@ -104,7 +119,7 @@
-
- Bit Parameter
+ Parameter
@@ -184,7 +199,7 @@
-
-
+
0
@@ -200,9 +215,6 @@
QAbstractItemView::AllEditTriggers
-
- true
-
diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui
index 70650bf817..805f7a3e3d 100644
--- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui
@@ -173,6 +173,12 @@
QFrame::Box
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
-
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index e26c2fe0da..20e35d8590 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -154,6 +154,10 @@ class PathWorkbench (Workbench):
if extracmdlist:
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], extracmdlist)
+ self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], ["Separator"])
+ self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP("Path", "Utils")],
+ ["Path_PropertyBag"])
+
self.dressupcmds = dressupcmdlist
curveAccuracy = PathPreferences.defaultLibAreaCurveAccuracy()
diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py
index 85dbb81e0a..c768fd53ab 100644
--- a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py
+++ b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathCustomGui.py b/src/Mod/Path/PathScripts/PathCustomGui.py
index 987bcfb50e..658468a4e0 100644
--- a/src/Mod/Path/PathScripts/PathCustomGui.py
+++ b/src/Mod/Path/PathScripts/PathCustomGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathCustom as PathCustom
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py
index 1b0a0da690..d98642f4dd 100644
--- a/src/Mod/Path/PathScripts/PathDeburrGui.py
+++ b/src/Mod/Path/PathScripts/PathDeburrGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathDeburr as PathDeburr
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py
index 34cffb1d68..6152c51433 100644
--- a/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py
+++ b/src/Mod/Path/PathScripts/PathDressupPathBoundaryGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathDressupPathBoundary as PathDressupPathBoundary
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathDressupTagGui.py b/src/Mod/Path/PathScripts/PathDressupTagGui.py
index 0a6c01dc6b..ee7c0119a6 100644
--- a/src/Mod/Path/PathScripts/PathDressupTagGui.py
+++ b/src/Mod/Path/PathScripts/PathDressupTagGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGeom as PathGeom
import PathScripts.PathGetPoint as PathGetPoint
import PathScripts.PathDressupHoldingTags as PathDressupTag
diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py
index 534d44e1a8..4ebee90d5f 100644
--- a/src/Mod/Path/PathScripts/PathDrillingGui.py
+++ b/src/Mod/Path/PathScripts/PathDrillingGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
import PathScripts.PathDrilling as PathDrilling
import PathScripts.PathGui as PathGui
diff --git a/src/Mod/Path/PathScripts/PathEngraveGui.py b/src/Mod/Path/PathScripts/PathEngraveGui.py
index 95d5ce9dc5..6301c4882d 100644
--- a/src/Mod/Path/PathScripts/PathEngraveGui.py
+++ b/src/Mod/Path/PathScripts/PathEngraveGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathEngrave as PathEngrave
import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathGui.py b/src/Mod/Path/PathScripts/PathGui.py
index c9fee42062..fb37725bf3 100644
--- a/src/Mod/Path/PathScripts/PathGui.py
+++ b/src/Mod/Path/PathScripts/PathGui.py
@@ -48,6 +48,7 @@ def updateInputField(obj, prop, widget, onBeforeChange=None):
If onBeforeChange is specified it is called before a new value is assigned to the property.
Returns True if a new value was assigned, False otherwise (new value is the same as the current).
'''
+ PathLog.track()
value = widget.property('rawValue')
attr = PathUtil.getProperty(obj, prop)
attrValue = attr.Value if hasattr(attr, 'Value') else attr
@@ -98,10 +99,12 @@ class QuantitySpinBox:
PathLog.track(widget)
self.widget = widget
self.onBeforeChange = onBeforeChange
+ self.prop = None
self.attachTo(obj, prop)
def attachTo(self, obj, prop = None):
'''attachTo(obj, prop=None) ... use an existing editor for the given object and property'''
+ PathLog.track(self.prop, prop)
self.obj = obj
self.prop = prop
if obj and prop:
@@ -119,12 +122,14 @@ class QuantitySpinBox:
def expression(self):
'''expression() ... returns the expression if one is bound to the property'''
+ PathLog.track(self.prop, self.valid)
if self.valid:
return self.widget.property('expression')
return ''
def setMinimum(self, quantity):
'''setMinimum(quantity) ... set the minimum'''
+ PathLog.track(self.prop, self.valid)
if self.valid:
value = quantity.Value if hasattr(quantity, 'Value') else quantity
self.widget.setProperty('setMinimum', value)
@@ -133,6 +138,7 @@ class QuantitySpinBox:
'''updateSpinBox(quantity=None) ... update the display value of the spin box.
If no value is provided the value of the bound property is used.
quantity can be of type Quantity or Float.'''
+ PathLog.track(self.prop, self.valid)
if self.valid:
if quantity is None:
quantity = PathUtil.getProperty(self.obj, self.prop)
@@ -141,6 +147,7 @@ class QuantitySpinBox:
def updateProperty(self):
'''updateProperty() ... update the bound property with the value from the spin box'''
+ PathLog.track(self.prop, self.valid)
if self.valid:
return updateInputField(self.obj, self.prop, self.widget, self.onBeforeChange)
return None
diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py
index cd48d41309..405dcb4fcc 100644
--- a/src/Mod/Path/PathScripts/PathGuiInit.py
+++ b/src/Mod/Path/PathScripts/PathGuiInit.py
@@ -66,6 +66,7 @@ def Startup():
# from PathScripts import PathProfileEdgesGui
# from PathScripts import PathProfileFacesGui
from PathScripts import PathProfileGui
+ from PathScripts import PathPropertyBagGui
from PathScripts import PathSanity
from PathScripts import PathSetupSheetGui
from PathScripts import PathSimpleCopy
diff --git a/src/Mod/Path/PathScripts/PathHelixGui.py b/src/Mod/Path/PathScripts/PathHelixGui.py
index 141cce33d9..b65048ee9c 100644
--- a/src/Mod/Path/PathScripts/PathHelixGui.py
+++ b/src/Mod/Path/PathScripts/PathHelixGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
import PathScripts.PathHelix as PathHelix
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py
index f5d2fa9db2..5e78e944df 100644
--- a/src/Mod/Path/PathScripts/PathJobGui.py
+++ b/src/Mod/Path/PathScripts/PathJobGui.py
@@ -31,6 +31,7 @@ from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathJob as PathJob
import PathScripts.PathJobCmd as PathJobCmd
import PathScripts.PathJobDlg as PathJobDlg
diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py
index 87e5f9b1e6..0aa304a5fc 100644
--- a/src/Mod/Path/PathScripts/PathOpGui.py
+++ b/src/Mod/Path/PathScripts/PathOpGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGeom as PathGeom
import PathScripts.PathGetPoint as PathGetPoint
import PathScripts.PathGui as PathGui
diff --git a/src/Mod/Path/PathScripts/PathPocketBaseGui.py b/src/Mod/Path/PathScripts/PathPocketBaseGui.py
index 915b6ea71a..68920b0a5e 100644
--- a/src/Mod/Path/PathScripts/PathPocketBaseGui.py
+++ b/src/Mod/Path/PathScripts/PathPocketBaseGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGui as PathGui
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathPocketShapeGui.py b/src/Mod/Path/PathScripts/PathPocketShapeGui.py
index 53bc26c336..4e1b651148 100644
--- a/src/Mod/Path/PathScripts/PathPocketShapeGui.py
+++ b/src/Mod/Path/PathScripts/PathPocketShapeGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGeom as PathGeom
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py
index d914568631..a1c1e58fc5 100644
--- a/src/Mod/Path/PathScripts/PathPreferences.py
+++ b/src/Mod/Path/PathScripts/PathPreferences.py
@@ -151,26 +151,9 @@ def searchPathsPost():
return paths
-def searchPathsTool(sub='Bit'):
+def searchPathsTool(sub):
paths = []
-
- if 'Bit' == sub:
- paths.append("{}/Bit".format(os.path.dirname(lastPathToolLibrary())))
- paths.append(lastPathToolBit())
-
- if 'Library' == sub:
- paths.append(lastPathToolLibrary())
- if 'Shape' == sub:
- paths.append(lastPathToolShape())
-
- def appendPath(p, sub):
- if p:
- paths.append(os.path.join(p, 'Tools', sub))
- paths.append(os.path.join(p, sub))
- paths.append(p)
- appendPath(defaultFilePath(), sub)
- appendPath(macroFilePath(), sub)
- appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub)
+ paths.append(os.path.join(FreeCAD.getHomePath(), 'Mod', 'Path', 'Tools', sub))
return paths
@@ -263,15 +246,19 @@ def setDefaultTaskPanelLayout(style):
def experimentalFeaturesEnabled():
return preferences().GetBool(EnableExperimentalFeatures, False)
+
def suppressAllSpeedsWarning():
return preferences().GetBool(WarningSuppressAllSpeeds, True)
+
def suppressRapidSpeedsWarning():
return suppressAllSpeedsWarning() or preferences().GetBool(WarningSuppressRapidSpeeds, True)
+
def suppressSelectionModeWarning():
return preferences().GetBool(WarningSuppressSelectionMode, True)
+
def suppressOpenCamLibWarning():
return preferences().GetBool(WarningSuppressOpenCamLib, True)
@@ -316,7 +303,8 @@ def lastPathToolLibrary():
def setLastPathToolLibrary(path):
PathLog.track(path)
curLib = lastFileToolLibrary()
- if os.path.split(curLib)[0] != path:
+ PathLog.debug('curLib: {}'.format(curLib))
+ if curLib and os.path.split(curLib)[0] != path:
setLastFileToolLibrary('') # a path is known but not specific file
return preferences().SetString(LastPathToolLibrary, path)
diff --git a/src/Mod/Path/PathScripts/PathProbeGui.py b/src/Mod/Path/PathScripts/PathProbeGui.py
index a00ec913e8..29c97d7adc 100644
--- a/src/Mod/Path/PathScripts/PathProbeGui.py
+++ b/src/Mod/Path/PathScripts/PathProbeGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathProbe as PathProbe
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathGui as PathGui
diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py
index a7f77d7062..8a85a6abfa 100644
--- a/src/Mod/Path/PathScripts/PathProfileGui.py
+++ b/src/Mod/Path/PathScripts/PathProfileGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGui as PathGui
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathProfile as PathProfile
diff --git a/src/Mod/Path/PathScripts/PathProperty.py b/src/Mod/Path/PathScripts/PathProperty.py
new file mode 100644
index 0000000000..7e3eeb1e6f
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathProperty.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * 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 *
+# * *
+# ***************************************************************************
+
+import PathScripts.PathLog as PathLog
+
+__title__ = "Property type abstraction for editing purposes"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "https://www.freecadweb.org"
+__doc__ = "Prototype objects to allow extraction of setup sheet values and editing."
+
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+class Property(object):
+ '''Base class for all prototype properties'''
+ def __init__(self, name, propType, category, info):
+ self.name = name
+ self.propType = propType
+ self.category = category
+ self.info = info
+ self.editorMode = 0
+ self.value = None
+
+ def setValue(self, value):
+ self.value = value
+ def getValue(self):
+ return self.value
+
+ def setEditorMode(self, mode):
+ self.editorMode = mode
+
+ def displayString(self):
+ if self.value is None:
+ t = self.typeString()
+ p = 'an' if t[0] in ['A', 'E', 'I', 'O', 'U'] else 'a'
+ return "%s %s" % (p, t)
+ return self.value
+
+ def typeString(self):
+ return "Property"
+
+ def setupProperty(self, obj, name, category, value):
+ created = False
+ if not hasattr(obj, name):
+ obj.addProperty(self.propType, name, category, self.info)
+ self.initProperty(obj, name)
+ created = True
+ setattr(obj, name, value)
+ return created
+
+ def initProperty(self, obj, name):
+ pass
+
+ def setValueFromString(self, string):
+ self.setValue(self.valueFromString(string))
+
+ def valueFromString(self, string):
+ return string
+
+class PropertyEnumeration(Property):
+ def typeString(self):
+ return "Enumeration"
+
+ def setValue(self, value):
+ if list == type(value):
+ self.enums = value # pylint: disable=attribute-defined-outside-init
+ else:
+ super(PropertyEnumeration, self).setValue(value)
+
+ def getEnumValues(self):
+ return self.enums
+
+ def initProperty(self, obj, name):
+ setattr(obj, name, self.enums)
+
+class PropertyQuantity(Property):
+ def displayString(self):
+ if self.value is None:
+ return Property.displayString(self)
+ return self.value.getUserPreferred()[0]
+
+class PropertyAngle(PropertyQuantity):
+ def typeString(self):
+ return "Angle"
+
+class PropertyDistance(PropertyQuantity):
+ def typeString(self):
+ return "Distance"
+
+class PropertyLength(PropertyQuantity):
+ def typeString(self):
+ return "Length"
+
+class PropertyPercent(Property):
+ def typeString(self):
+ return "Percent"
+
+class PropertyFloat(Property):
+ def typeString(self):
+ return "Float"
+
+ def valueFromString(self, string):
+ return float(string)
+
+class PropertyInteger(Property):
+ def typeString(self):
+ return "Integer"
+
+ def valueFromString(self, string):
+ return int(string)
+
+class PropertyBool(Property):
+ def typeString(self):
+ return "Bool"
+
+ def valueFromString(self, string):
+ return bool(string)
+
+class PropertyString(Property):
+ def typeString(self):
+ return "String"
+
+class PropertyMap(Property):
+ def typeString(self):
+ return "Map"
+
+ def displayString(self, value):
+ return str(value)
+
+class OpPrototype(object):
+
+ PropertyType = {
+ 'App::PropertyAngle': PropertyAngle,
+ 'App::PropertyBool': PropertyBool,
+ 'App::PropertyDistance': PropertyDistance,
+ 'App::PropertyEnumeration': PropertyEnumeration,
+ 'App::PropertyFile': PropertyString,
+ 'App::PropertyFloat': PropertyFloat,
+ 'App::PropertyFloatConstraint': Property,
+ 'App::PropertyFloatList': Property,
+ 'App::PropertyInteger': PropertyInteger,
+ 'App::PropertyIntegerList': PropertyInteger,
+ 'App::PropertyLength': PropertyLength,
+ 'App::PropertyLink': Property,
+ 'App::PropertyLinkList': Property,
+ 'App::PropertyLinkSubListGlobal': Property,
+ 'App::PropertyMap': PropertyMap,
+ 'App::PropertyPercent': PropertyPercent,
+ 'App::PropertyString': PropertyString,
+ 'App::PropertyStringList': Property,
+ 'App::PropertyVectorDistance': Property,
+ 'App::PropertyVectorList': Property,
+ 'Part::PropertyPartShape': Property,
+ }
+
+ def __init__(self, name):
+ self.Label = name
+ self.properties = {}
+ self.DoNotSetDefaultValues = True
+ self.Proxy = None
+
+ def __setattr__(self, name, val):
+ if name in ['Label', 'DoNotSetDefaultValues', 'properties', 'Proxy']:
+ if name == 'Proxy':
+ val = None # make sure the proxy is never set
+ return super(OpPrototype, self).__setattr__(name, val)
+ self.properties[name].setValue(val)
+
+ def addProperty(self, typeString, name, category, info = None):
+ prop = self.PropertyType[typeString](name, typeString, category, info)
+ self.properties[name] = prop
+ return self
+
+ def setEditorMode(self, name, mode):
+ self.properties[name].setEditorMode(mode)
+
+ def getProperty(self, name):
+ return self.properties[name]
+
+ def setupProperties(self, setup):
+ return [p for p in self.properties if p.name in setup]
diff --git a/src/Mod/Path/PathScripts/PathPropertyBag.py b/src/Mod/Path/PathScripts/PathPropertyBag.py
new file mode 100644
index 0000000000..2b21be2c6b
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathPropertyBag.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * 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 *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import PySide
+
+__title__ = 'Generic property container to store some values.'
+__author__ = 'sliptonic (Brad Collette)'
+__url__ = 'https://www.freecadweb.org'
+__doc__ = 'A generic container for typed properties in arbitrary categories.'
+
+def translate(context, text, disambig=None):
+ return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+SupportedPropertyType = {
+ 'Angle' : 'App::PropertyAngle',
+ 'Bool' : 'App::PropertyBool',
+ 'Distance' : 'App::PropertyDistance',
+ 'Enumeration' : 'App::PropertyEnumeration',
+ 'File' : 'App::PropertyFile',
+ 'Float' : 'App::PropertyFloat',
+ 'Integer' : 'App::PropertyInteger',
+ 'Length' : 'App::PropertyLength',
+ 'Percent' : 'App::PropertyPercent',
+ 'String' : 'App::PropertyString',
+ }
+
+def getPropertyTypeName(o):
+ for typ in SupportedPropertyType:
+ if SupportedPropertyType[typ] == o:
+ return typ
+ raise IndexError()
+
+class PropertyBag(object):
+ '''Property container object.'''
+
+ CustomPropertyGroups = 'CustomPropertyGroups'
+ CustomPropertyGroupDefault = 'User'
+
+ def __init__(self, obj):
+ obj.addProperty('App::PropertyStringList', self.CustomPropertyGroups, 'Base', PySide.QtCore.QT_TRANSLATE_NOOP('PathPropertyBag', 'List of custom property groups'))
+ self.onDocumentRestored(obj)
+
+ def __getstate__(self):
+ return None
+
+ def __setstate__(self, state):
+ return None
+
+ def onDocumentRestored(self, obj):
+ self.obj = obj
+ obj.setEditorMode(self.CustomPropertyGroups, 2) # hide
+
+ def getCustomProperties(self):
+ '''getCustomProperties() ... Return a list of all custom properties created in this container.'''
+ return [p for p in self.obj.PropertiesList if self.obj.getGroupOfProperty(p) in self.obj.CustomPropertyGroups]
+
+ def addCustomProperty(self, propertyType, name, group=None, desc=None):
+ '''addCustomProperty(propertyType, name, group=None, desc=None) ... adds a custom property and tracks its group.'''
+ if desc is None:
+ desc = ''
+ if group is None:
+ group = self.CustomPropertyGroupDefault
+ groups = self.obj.CustomPropertyGroups
+ if not group in groups:
+ groups.append(group)
+ self.obj.CustomPropertyGroups = groups
+ self.obj.addProperty(propertyType, name, group, desc)
+
+ def refreshCustomPropertyGroups(self):
+ '''refreshCustomPropertyGroups() ... removes empty property groups, should be called after deleting properties.'''
+ customGroups = []
+ for p in self.obj.PropertiesList:
+ group = self.obj.getGroupOfProperty(p)
+ if group in self.obj.CustomPropertyGroups and not group in customGroups:
+ customGroups.append(group)
+ self.obj.CustomPropertyGroups = customGroups
+
+
+def Create(name = 'PropertyBag'):
+ obj = FreeCAD.ActiveDocument.addObject('App::FeaturePython', name)
+ obj.Proxy = PropertyBag(obj)
+ return obj
+
+def IsPropertyBag(obj):
+ '''Returns True if the supplied object is a property container (or its Proxy).'''
+
+ if type(obj) == PropertyBag:
+ return True
+ if hasattr(obj, 'Proxy'):
+ return IsPropertyBag(obj.Proxy)
+ return False
+
diff --git a/src/Mod/Path/PathScripts/PathPropertyBagGui.py b/src/Mod/Path/PathScripts/PathPropertyBagGui.py
new file mode 100644
index 0000000000..54f55d4a56
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathPropertyBagGui.py
@@ -0,0 +1,425 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * 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 *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
+import PathScripts.PathIconViewProvider as PathIconViewProvider
+import PathScripts.PathLog as PathLog
+import PathScripts.PathPropertyBag as PathPropertyBag
+import PathScripts.PathPropertyEditor as PathPropertyEditor
+import PathScripts.PathUtil as PathUtil
+
+from PySide import QtCore, QtGui
+
+__title__ = "Property Bag Editor"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "https://www.freecadweb.org"
+__doc__ = "Task panel editor for a PropertyBag"
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+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):
+ PathLog.track(name)
+ vobj.Proxy = self
+ self.icon = name
+ # mode = 2
+ self.obj = None
+ self.vobj = None
+
+ def attach(self, vobj):
+ PathLog.track()
+ self.vobj = vobj
+ self.obj = vobj.Object
+
+ def getIcon(self):
+ return ":/icons/Path-SetupSheet.svg"
+
+ def __getstate__(self):
+ return None
+
+ def __setstate__(self, state):
+ # pylint: disable=unused-argument
+ return None
+
+ def getDisplayMode(self, mode):
+ # pylint: disable=unused-argument
+ return 'Default'
+
+ def setEdit(self, vobj, mode=0):
+ # pylint: disable=unused-argument
+ PathLog.track()
+ taskPanel = TaskPanel(vobj)
+ FreeCADGui.Control.closeDialog()
+ FreeCADGui.Control.showDialog(taskPanel)
+ taskPanel.setupUi()
+ return True
+
+ def unsetEdit(self, vobj, mode):
+ # pylint: disable=unused-argument
+ 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):
+ # #PathLog.track(index.column(), type(option))
+
+ def createEditor(self, parent, option, index):
+ # pylint: disable=unused-argument
+ 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):
+ PathLog.track(index.row(), index.column())
+ index.data(self.RoleEditor).setEditorData(widget)
+
+ def setModelData(self, widget, model, index):
+ # pylint: disable=unused-argument
+ PathLog.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):
+ # pylint: disable=unused-argument
+ 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()
+ for g in sorted(obj.CustomPropertyGroups):
+ 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 typeSet and self.propertyName() 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_()
+
+Panel = []
+
+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(translate("PathPropertyBag", "Edit PropertyBag"))
+ Panel.append(self)
+
+ def updateData(self, topLeft, bottomRight):
+ pass
+
+
+ def _setupProperty(self, i, name):
+ typ = PathPropertyBag.getPropertyTypeName(self.obj.getTypeIdOfProperty(name))
+ 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):
+ PathLog.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):
+ PathLog.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()
+ self.obj.Proxy.addCustomProperty(typ, name, grp, info)
+ if dialog.propertyIsEnumeration():
+ setattr(self.obj, name, dialog.propertyEnumerations())
+ return (name, info)
+
+ def propertyAdd(self):
+ PathLog.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)
+ 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
+ if not more:
+ break
+
+ def propertyModifyIndex(self, index):
+ PathLog.track(index.row(), index.column())
+ row = index.row()
+
+ obj = self.model.item(row, self.ColumnVal).data(Delegate.RoleObject)
+ nam = self.model.item(row, self.ColumnVal).data(Delegate.RoleProperty)
+ grp = obj.getGroupOfProperty(nam)
+ typ = obj.getTypeIdOfProperty(nam)
+
+ dialog = PropertyCreate(self.obj, grp, typ, False)
+ if dialog.exec_(nam):
+ val = getattr(obj, nam)
+ obj.removeProperty(nam)
+ name, info = self.addCustomProperty(self.obj, dialog)
+ try:
+ setattr(obj, nam, val)
+ except:
+ # this can happen if the old enumeration value doesn't exist anymore
+ pass
+ newVal = PathUtil.getPropertyValueString(obj, nam)
+ 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):
+ PathLog.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):
+ PathLog.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(translate("PathPropertyBag", "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('PathPropertyBag', 'Property Bag'),
+ 'ToolTip': translate('PathPropertyBag', '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('Path_PropertyBag', PropertyBagCreateCommand())
+
+FreeCAD.Console.PrintLog("Loading PathPropertyBagGui ... done\n")
diff --git a/src/Mod/Path/PathScripts/PathPropertyEditor.py b/src/Mod/Path/PathScripts/PathPropertyEditor.py
new file mode 100644
index 0000000000..f376e351fd
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathPropertyEditor.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * 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 *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import PathScripts.PathLog as PathLog
+import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
+
+from PySide import QtCore, QtGui
+
+__title__ = "Path Property Editor"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "https://www.freecadweb.org"
+__doc__ = "Task panel editor for Properties"
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+
+class _PropertyEditor(object):
+ '''Base class of all property editors - just outlines the TableView delegate interface.'''
+ def __init__(self, obj, prop):
+ self.obj = obj
+ self.prop = prop
+
+ def widget(self, parent):
+ '''widget(parent) ... called by the delegate to get a new editor widget.
+ Must be implemented by subclasses and return the widget.'''
+ pass # pylint: disable=unnecessary-pass
+
+ def setEditorData(self, widget):
+ '''setEditorData(widget) ... called by the delegate to initialize the editor.
+ The widget is the object returned by widget().
+ Must be implemented by subclasses.'''
+ pass # pylint: disable=unnecessary-pass
+
+ def setModelData(self, widget):
+ '''setModelData(widget) ... called by the delegate to store new values.
+ Must be implemented by subclasses.'''
+ pass # pylint: disable=unnecessary-pass
+
+ def propertyValue(self):
+ return self.obj.getPropertyByName(self.prop)
+
+ def setProperty(self, value):
+ setattr(self.obj, self.prop, value)
+
+ def displayString(self):
+ return self.propertyValue()
+
+class _PropertyEditorBool(_PropertyEditor):
+ '''Editor for boolean values - uses a combo box.'''
+
+ def widget(self, parent):
+ return QtGui.QComboBox(parent)
+
+ def setEditorData(self, widget):
+ widget.clear()
+ widget.addItems([str(False), str(True)])
+ index = 1 if self.propertyValue() else 0
+ widget.setCurrentIndex(index)
+
+ def setModelData(self, widget):
+ self.setProperty(widget.currentText() == str(True))
+
+class _PropertyEditorString(_PropertyEditor):
+ '''Editor for string values - uses a line edit.'''
+
+ def widget(self, parent):
+ return QtGui.QLineEdit(parent)
+
+ def setEditorData(self, widget):
+ text = '' if self.propertyValue() is None else self.propertyValue()
+ widget.setText(text)
+
+ def setModelData(self, widget):
+ self.setProperty(widget.text())
+
+class _PropertyEditorQuantity(_PropertyEditor):
+
+ def widget(self, parent):
+ return QtGui.QLineEdit(parent)
+
+ def setEditorData(self, widget):
+ quantity = self.propertyValue()
+ if quantity is None:
+ quantity = self.defaultQuantity()
+ widget.setText(quantity.getUserPreferred()[0])
+
+ def defaultQuantity(self):
+ pass
+
+ def setModelData(self, widget):
+ self.setProperty(FreeCAD.Units.Quantity(widget.text()))
+
+ def displayString(self):
+ if self.propertyValue() is None:
+ return ''
+ return self.propertyValue().getUserPreferred()[0]
+
+class _PropertyEditorAngle(_PropertyEditorQuantity):
+ '''Editor for angle values - uses a line edit'''
+
+ def defaultQuantity(self):
+ return FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle)
+
+class _PropertyEditorLength(_PropertyEditorQuantity):
+ '''Editor for length values - uses a line edit.'''
+
+ def defaultQuantity(self):
+ return FreeCAD.Units.Quantity(0, FreeCAD.Units.Length)
+
+class _PropertyEditorPercent(_PropertyEditor):
+ '''Editor for percent values - uses a spin box.'''
+
+ def widget(self, parent):
+ return QtGui.QSpinBox(parent)
+
+ def setEditorData(self, widget):
+ widget.setRange(0, 100)
+ value = self.propertyValue()
+ if value is None:
+ value = 0
+ widget.setValue(value)
+
+ def setModelData(self, widget):
+ self.setProperty(widget.value())
+
+class _PropertyEditorInteger(_PropertyEditor):
+ '''Editor for integer values - uses a spin box.'''
+
+ def widget(self, parent):
+ return QtGui.QSpinBox(parent)
+
+ def setEditorData(self, widget):
+ value = self.propertyValue()
+ if value is None:
+ value = 0
+ widget.setValue(value)
+
+ def setModelData(self, widget):
+ self.setProperty(widget.value())
+
+class _PropertyEditorFloat(_PropertyEditor):
+ '''Editor for float values - uses a double spin box.'''
+
+ def widget(self, parent):
+ return QtGui.QDoubleSpinBox(parent)
+
+ def setEditorData(self, widget):
+ value = self.propertyValue()
+ if value is None:
+ value = 0.0
+ widget.setValue(value)
+
+ def setModelData(self, widget):
+ self.setProperty(widget.value())
+
+class _PropertyEditorFile(_PropertyEditor):
+
+ def widget(self, parent):
+ return QtGui.QLineEdit(parent)
+
+ def setEditorData(self, widget):
+ text = '' if self.propertyValue() is None else self.propertyValue()
+ widget.setText(text)
+
+ def setModelData(self, widget):
+ self.setProperty(widget.text())
+
+class _PropertyEditorEnumeration(_PropertyEditor):
+
+ def widget(self, parent):
+ return QtGui.QComboBox(parent)
+
+ def setEditorData(self, widget):
+ widget.clear()
+ widget.addItems(self.obj.getEnumerationsOfProperty(self.prop))
+ widget.setCurrentText(self.propertyValue())
+
+ def setModelData(self, widget):
+ self.setProperty(widget.currentText())
+
+_EditorFactory = {
+ 'App::PropertyAngle' : _PropertyEditorAngle,
+ 'App::PropertyBool' : _PropertyEditorBool,
+ 'App::PropertyDistance' : _PropertyEditorLength,
+ 'App::PropertyEnumeration' : _PropertyEditorEnumeration,
+ #'App::PropertyFile' : _PropertyEditorFile,
+ 'App::PropertyFloat' : _PropertyEditorFloat,
+ 'App::PropertyInteger' : _PropertyEditorInteger,
+ 'App::PropertyLength' : _PropertyEditorLength,
+ 'App::PropertyPercent' : _PropertyEditorPercent,
+ 'App::PropertyString' : _PropertyEditorString,
+ }
+
+def Types():
+ '''Return the types of properties supported.'''
+ return [t for t in _EditorFactory]
+
+def Editor(obj, prop):
+ '''Returns an editor class to be used for the given property.'''
+ factory = _EditorFactory[obj.getTypeIdOfProperty(prop)]
+ if factory:
+ return factory(obj, prop)
+ return None
diff --git a/src/Mod/Path/PathScripts/PathSetupSheetGui.py b/src/Mod/Path/PathScripts/PathSetupSheetGui.py
index d2ceb3956b..1584b964a0 100644
--- a/src/Mod/Path/PathScripts/PathSetupSheetGui.py
+++ b/src/Mod/Path/PathScripts/PathSetupSheetGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathGui as PathGui
import PathScripts.PathIconViewProvider as PathIconViewProvider
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py
index 086db6b145..3a01944abc 100644
--- a/src/Mod/Path/PathScripts/PathSimulatorGui.py
+++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import Path
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathDressup as PathDressup
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathSlotGui.py b/src/Mod/Path/PathScripts/PathSlotGui.py
index 397ec14fdf..518209e562 100644
--- a/src/Mod/Path/PathScripts/PathSlotGui.py
+++ b/src/Mod/Path/PathScripts/PathSlotGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathSlot as PathSlot
import PathScripts.PathGui as PathGui
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index 038a670c52..25a5cea8c7 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathSurface as PathSurface
import PathScripts.PathGui as PathGui
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathThreadMillingGui.py b/src/Mod/Path/PathScripts/PathThreadMillingGui.py
index fab21692a7..885f129a6b 100644
--- a/src/Mod/Path/PathScripts/PathThreadMillingGui.py
+++ b/src/Mod/Path/PathScripts/PathThreadMillingGui.py
@@ -23,6 +23,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
import PathScripts.PathThreadMilling as PathThreadMilling
import PathScripts.PathGui as PathGui
diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py
index 5ffe7503dd..38e8a4ce4e 100644
--- a/src/Mod/Path/PathScripts/PathToolBit.py
+++ b/src/Mod/Path/PathScripts/PathToolBit.py
@@ -24,6 +24,7 @@ import FreeCAD
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
+import PathScripts.PathPropertyBag as PathPropertyBag
import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype
import PathScripts.PathUtil as PathUtil
import PySide
@@ -42,74 +43,73 @@ __author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecadweb.org"
__doc__ = "Class to deal with and represent a tool bit."
-# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
-# PathLog.trackModule()
+PropertyGroupShape = 'Shape'
+
+_DebugFindTool = False
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule()
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
+def _findToolFile(name, containerFile, typ):
+ PathLog.track(name)
+ if os.path.exists(name): # absolute reference
+ return name
-ParameterTypeConstraint = {
- 'Angle': 'App::PropertyAngle',
- 'Distance': 'App::PropertyLength',
- 'DistanceX': 'App::PropertyLength',
- 'DistanceY': 'App::PropertyLength',
- 'Radius': 'App::PropertyLength'}
+ if containerFile:
+ rootPath = os.path.dirname(os.path.dirname(containerFile))
+ paths = [os.path.join(rootPath, typ)]
+ else:
+ paths = []
+ paths.extend(PathPreferences.searchPathsTool(typ))
+
+ def _findFile(path, name):
+ PathLog.track(path, name)
+ fullPath = os.path.join(path, name)
+ if os.path.exists(fullPath):
+ return (True, fullPath)
+ for root, ds, fs in os.walk(path):
+ for d in ds:
+ found, fullPath = _findFile(d, name)
+ if found:
+ return (True, fullPath)
+ return (False, None)
-def _findTool(path, typ, dbg=False):
- if os.path.exists(path): # absolute reference
- if dbg:
- PathLog.debug("Found {} at {}".format(typ, path))
- return path
-
- def searchFor(pname, fname):
- # PathLog.debug("pname: {} fname: {}".format(pname, fname))
- if dbg:
- PathLog.debug("Looking for {}".format(pname))
- if fname:
- for p in PathPreferences.searchPathsTool(typ):
- PathLog.track(p)
- f = os.path.join(p, fname)
- if dbg:
- PathLog.debug(" Checking {}".format(f))
- if os.path.exists(f):
- if dbg:
- PathLog.debug(" Found {} at {}".format(typ, f))
- return f
- if pname and os.path.sep != pname:
- PathLog.track(pname)
- ppname, pfname = os.path.split(pname)
- ffname = os.path.join(pfname, fname) if fname else pfname
- return searchFor(ppname, ffname)
- return None
-
- return searchFor(path, '')
+ for p in paths:
+ found, path = _findFile(p, name)
+ if found:
+ return path
+ return None
-def findShape(path):
- '''
- findShape(path) ... search for path, full and partially
- in all known shape directories.
- '''
- return _findTool(path, 'Shape')
+def findToolShape(name, path=None):
+ '''findToolShape(name, path) ... search for name, if relative path look in path'''
+ PathLog.track(name, path)
+ return _findToolFile(name, path, 'Shape')
-def findBit(path):
- PathLog.track(path)
- if path.endswith('.fctb'):
- return _findTool(path, 'Bit')
- return _findTool("{}.fctb".format(path), 'Bit')
+def findToolBit(name, path=None):
+ '''findToolBit(name, path) ... search for name, if relative path look in path'''
+ PathLog.track(name, path)
+ if name.endswith('.fctb'):
+ return _findToolFile(name, path, 'Bit')
+ return _findToolFile("{}.fctb".format(name), path, 'Bit')
-def findLibrary(path, dbg=False):
- if path.endswith('.fctl'):
- return _findTool(path, 'Library', dbg)
- return _findTool("{}.fctl".format(path), 'Library', dbg)
+def findToolLibrary(name, path=None):
+ '''findToolLibrary(name, path) ... search for name, if relative path look in path'''
+ PathLog.track(name, path)
+ if name.endswith('.fctl'):
+ return _findToolFile(name, path, 'Library')
+ return _findToolFile("{}.fctl".format(name), path, 'Library')
def _findRelativePath(path, typ):
+ PathLog.track(path, typ)
relative = path
for p in PathPreferences.searchPathsTool(typ):
if path.startswith(p):
@@ -132,48 +132,19 @@ def findRelativePathTool(path):
def findRelativePathLibrary(path):
return _findRelativePath(path, 'Library')
-
-def updateConstraint(sketch, name, value):
- for i, constraint in enumerate(sketch.Constraints):
- if constraint.Name.split(';')[0] == name:
- constr = None
- if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']:
- constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value)
- else:
- print(constraint.Name, constraint.Type)
-
- if constr is not None:
- if not PathGeom.isRoughly(constraint.Value, value.Value):
- PathLog.track(name, constraint.Type,
- 'update', i, "(%.2f -> %.2f)"
- % (constraint.Value, value.Value))
- sketch.delConstraint(i)
- sketch.recompute()
- n = sketch.addConstraint(constr)
- sketch.renameConstraint(n, constraint.Name)
- else:
- PathLog.track(name, constraint.Type, 'unchanged')
- break
-
-
-PropertyGroupBit = 'Bit'
-PropertyGroupAttribute = 'Attribute'
-
-
class ToolBit(object):
- def __init__(self, obj, shapeFile):
- PathLog.track(obj.Label, shapeFile)
+ def __init__(self, obj, shapeFile, path=None):
+ PathLog.track(obj.Label, shapeFile, path)
self.obj = obj
- obj.addProperty('App::PropertyFile', 'BitShape', 'Base',
- translate('PathToolBit', 'Shape for bit shape'))
- obj.addProperty('App::PropertyLink', 'BitBody', 'Base',
- translate('PathToolBit',
- 'The parametrized body representing the tool bit'))
- obj.addProperty('App::PropertyFile', 'File', 'Base',
- translate('PathToolBit', 'The file of the tool'))
- obj.addProperty('App::PropertyString', 'ShapeName', 'Base',
- translate('PathToolBit', 'The name of the shape file'))
+ obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape'))
+ obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit'))
+ obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool'))
+ obj.addProperty('App::PropertyString', 'ShapeName', 'Base', translate('PathToolBit', 'The name of the shape file'))
+ obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit'))
+
+ if path:
+ obj.File = path
if shapeFile is None:
obj.BitShape = 'endmill.fcstd'
self._setupBitShape(obj)
@@ -193,12 +164,6 @@ class ToolBit(object):
break
return None
- def propertyNamesBit(self, obj):
- return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit]
-
- def propertyNamesAttribute(self, obj):
- return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute]
-
def onDocumentRestored(self, obj):
# when files are shared it is essential to be able to change/set the shape file,
# otherwise the file is hard to use
@@ -206,19 +171,39 @@ class ToolBit(object):
obj.setEditorMode('BitBody', 2)
obj.setEditorMode('File', 1)
obj.setEditorMode('Shape', 2)
+ if not hasattr(obj, 'BitPropertyNames'):
+ obj.addProperty('App::PropertyStringList', 'BitPropertyNames', 'Base', translate('PathToolBit', 'List of all properties inherited from the bit'))
+ propNames = []
+ for prop in obj.PropertiesList:
+ if obj.getGroupOfProperty(prop) == 'Bit':
+ val = obj.getPropertyByName(prop)
+ typ = obj.getTypeIdOfProperty(prop)
+ dsc = obj.getDocumentationOfProperty(prop)
- for prop in self.propertyNamesBit(obj):
- obj.setEditorMode(prop, 1)
- # I currently don't see why these need to be read-only
- # for prop in self.propertyNamesAttribute(obj):
- # obj.setEditorMode(prop, 1)
+ obj.removeProperty(prop)
+ obj.addProperty(typ, prop, PropertyGroupShape, dsc)
+
+ PathUtil.setProperty(obj, prop, val)
+ propNames.append(prop)
+ elif obj.getGroupOfProperty(prop) == 'Attribute':
+ propNames.append(prop)
+ obj.BitPropertyNames = propNames
+ obj.setEditorMode('BitPropertyNames', 2)
+
+ for prop in obj.BitPropertyNames:
+ if obj.getGroupOfProperty(prop) == PropertyGroupShape:
+ # properties in the Shape group can only be modified while the actual
+ # shape is loaded, so we have to disable direct property editing
+ obj.setEditorMode(prop, 1)
+ else:
+ # all other custom properties can and should be edited directly in the
+ # property editor widget, not much value in re-implementing that
+ obj.setEditorMode(prop, 0)
def onChanged(self, obj, prop):
PathLog.track(obj.Label, prop)
if prop == 'BitShape' and 'Restore' not in obj.State:
self._setupBitShape(obj)
- # elif obj.getGroupOfProperty(prop) == PropertyGroupBit:
- # self._updateBitShape(obj, [prop])
def onDelete(self, obj, arg2=None):
PathLog.track(obj.Label)
@@ -227,12 +212,19 @@ class ToolBit(object):
def _updateBitShape(self, obj, properties=None):
if obj.BitBody is not None:
- if not properties:
- properties = self.propertyNamesBit(obj)
- for prop in properties:
- for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
- PathLog.track(obj.Label, sketch.Label, prop)
- updateConstraint(sketch, prop, obj.getPropertyByName(prop))
+ for attributes in [o for o in obj.BitBody.Group if hasattr(o, 'Proxy') and hasattr(o.Proxy, 'getCustomProperties')]:
+ for prop in attributes.Proxy.getCustomProperties():
+ # the property might not exist in our local object (new attribute in shape)
+ # for such attributes we just keep the default
+ if hasattr(obj, prop):
+ setattr(attributes, prop, obj.getPropertyByName(prop))
+ else:
+ # if the template shape has a new attribute defined we should add that
+ # to the local object
+ self._setupProperty(obj, prop, attributes)
+ propNames = obj.BitPropertyNames
+ propNames.append(prop)
+ obj.BitPropertyNames = propNames
self._copyBitShape(obj)
def _copyBitShape(self, obj):
@@ -243,6 +235,7 @@ class ToolBit(object):
obj.Shape = Part.Shape()
def _loadBitBody(self, obj, path=None):
+ PathLog.track(obj.Label, path)
p = path if path else obj.BitShape
docOpened = False
doc = None
@@ -251,12 +244,15 @@ class ToolBit(object):
doc = FreeCAD.getDocument(d)
break
if doc is None:
- p = findShape(p)
+ p = findToolShape(p, path if path else obj.File)
if not path and p != obj.BitShape:
obj.BitShape = p
+ PathLog.debug("ToolBit {} using shape file: {}".format(obj.Label, p))
doc = FreeCAD.openDocument(p, True)
obj.ShapeName = doc.Name
docOpened = True
+ else:
+ PathLog.debug("ToolBit {} already open: {}".format(obj.Label, doc))
return (doc, docOpened)
def _removeBitBody(self, obj):
@@ -269,7 +265,7 @@ class ToolBit(object):
PathLog.track(obj.Label)
self._removeBitBody(obj)
self._copyBitShape(obj)
- for prop in self.propertyNamesBit(obj):
+ for prop in obj.BitPropertyNames:
obj.removeProperty(prop)
def loadBitBody(self, obj, force=False):
@@ -287,6 +283,20 @@ class ToolBit(object):
def unloadBitBody(self, obj):
self._removeBitBody(obj)
+ def _setupProperty(self, obj, prop, orig):
+ # extract property parameters and values so it can be copied
+ val = orig.getPropertyByName(prop)
+ typ = orig.getTypeIdOfProperty(prop)
+ grp = orig.getGroupOfProperty(prop)
+ dsc = orig.getDocumentationOfProperty(prop)
+
+ obj.addProperty(typ, prop, grp, dsc)
+ if 'App::PropertyEnumeration' == typ:
+ setattr(obj, prop, orig.getEnumerationsOfProperty(prop))
+
+ obj.setEditorMode(prop, 1)
+ PathUtil.setProperty(obj, prop, val)
+
def _setupBitShape(self, obj, path=None):
PathLog.track(obj.Label)
@@ -296,6 +306,8 @@ class ToolBit(object):
obj.Label = doc.RootObjects[0].Label
self._deleteBitSetup(obj)
bitBody = obj.Document.copyObject(doc.RootObjects[0], True)
+
+ docName = doc.Name
if docOpened:
FreeCAD.setActiveDocument(activeDoc.Name)
FreeCAD.closeDocument(doc.Name)
@@ -303,29 +315,44 @@ class ToolBit(object):
if bitBody.ViewObject:
bitBody.ViewObject.Visibility = False
- for sketch in [o for o in bitBody.Group if o.TypeId == 'Sketcher::SketchObject']:
- for constraint in [c for c in sketch.Constraints if c.Name != '']:
- typ = ParameterTypeConstraint.get(constraint.Type)
- PathLog.track(constraint, typ)
- if typ is not None:
- parts = [p.strip() for p in constraint.Name.split(';')]
- prop = parts[0]
- desc = ''
- if len(parts) > 1:
- desc = parts[1]
- obj.addProperty(typ, prop, PropertyGroupBit, desc)
- obj.setEditorMode(prop, 1)
- value = constraint.Value
- if constraint.Type == 'Angle':
- value = value * 180 / math.pi
- PathUtil.setProperty(obj, prop, value)
+ PathLog.debug("bitBody.{} ({}): {}".format(bitBody.Label, bitBody.Name, type(bitBody)))
+
+ propNames = []
+ for attributes in [o for o in bitBody.Group if PathPropertyBag.IsPropertyBag(o)]:
+ PathLog.debug("Process properties from {}".format(attributes.Label))
+ for prop in attributes.Proxy.getCustomProperties():
+ self._setupProperty(obj, prop, attributes)
+ propNames.append(prop)
+ if not propNames:
+ PathLog.error(translate('PathToolBit', 'Did not find a PropertyBag in {} - not a ToolBit shape?').format(docName))
+
# has to happen last because it could trigger op.execute evaluations
+ obj.BitPropertyNames = propNames
obj.BitBody = bitBody
self._copyBitShape(obj)
+ def toolShapeProperties(self, obj):
+ '''toolShapeProperties(obj) ... return all properties defining it's shape'''
+ return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) == PropertyGroupShape])
+
+ def toolAdditionalProperties(self, obj):
+ '''toolShapeProperties(obj) ... return all properties unrelated to it's shape'''
+ return sorted([prop for prop in obj.BitPropertyNames if obj.getGroupOfProperty(prop) != PropertyGroupShape])
+
+ def toolGroupsAndProperties(self, obj, includeShape=True):
+ '''toolGroupsAndProperties(obj) ... returns a dictionary of group names with a list of property names.'''
+ category = {}
+ for prop in obj.BitPropertyNames:
+ group = obj.getGroupOfProperty(prop)
+ if includeShape or group != PropertyGroupShape:
+ properties = category.get(group, [])
+ properties.append(prop)
+ category[group] = properties
+ return category
+
def getBitThumbnail(self, obj):
if obj.BitShape:
- path = findShape(obj.BitShape)
+ path = findToolShape(obj.BitShape)
if path:
with open(path, 'rb') as fd:
try:
@@ -347,8 +374,7 @@ class ToolBit(object):
obj.File = path
return True
except (OSError, IOError) as e:
- PathLog.error("Could not save tool {} to {} ({})".format(
- obj.Label, path, e))
+ PathLog.error("Could not save tool {} to {} ({})".format(obj.Label, path, e))
raise
def templateAttrs(self, obj):
@@ -360,16 +386,10 @@ class ToolBit(object):
else:
attrs['shape'] = findRelativePathShape(obj.BitShape)
params = {}
- for name in self.propertyNamesBit(obj):
+ for name in obj.BitPropertyNames:
params[name] = PathUtil.getPropertyValueString(obj, name)
attrs['parameter'] = params
params = {}
- for name in self.propertyNamesAttribute(obj):
- if name == "UserAttributes":
- for key, value in obj.UserAttributes.items():
- params[key] = value
- else:
- params[name] = PathUtil.getPropertyValueString(obj, name)
attrs['attribute'] = params
return attrs
@@ -380,73 +400,33 @@ def Declaration(path):
return json.load(fp)
-class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype):
-
- def __init__(self):
- PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute')
- self.addProperty('App::PropertyEnumeration', 'Material',
- PropertyGroupAttribute,
- translate('PathToolBit', 'Tool bit material'))
- self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond',
- 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon']
- self.addProperty('App::PropertyDistance', 'LengthOffset',
- PropertyGroupAttribute, translate('PathToolBit',
- 'Length offset in Z direction'))
- self.addProperty('App::PropertyInteger', 'Flutes',
- PropertyGroupAttribute, translate('PathToolBit',
- 'The number of flutes'))
- self.addProperty('App::PropertyDistance', 'ChipLoad',
- PropertyGroupAttribute, translate('PathToolBit',
- 'Chipload as per manufacturer'))
- self.addProperty('App::PropertyMap', 'UserAttributes',
- PropertyGroupAttribute, translate('PathToolBit',
- 'User Defined Values'))
- self.addProperty('App::PropertyBool', 'SpindlePower',
- PropertyGroupAttribute, translate('PathToolBit',
- 'Whether Spindle Power should be allowed'))
-
-
class ToolBitFactory(object):
- def CreateFromAttrs(self, attrs, name='ToolBit'):
- PathLog.debug(attrs)
- obj = Factory.Create(name, attrs['shape'])
+ def CreateFromAttrs(self, attrs, name='ToolBit', path=None):
+ PathLog.track(attrs, path)
+ obj = Factory.Create(name, attrs['shape'], path)
obj.Label = attrs['name']
params = attrs['parameter']
for prop in params:
PathUtil.setProperty(obj, prop, params[prop])
obj.Proxy._updateBitShape(obj)
obj.Proxy.unloadBitBody(obj)
- params = attrs['attribute']
- proto = AttributePrototype()
- uservals = {}
- for pname in params:
- try:
- prop = proto.getProperty(pname)
- prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname]))
- except Exception:
- prop = proto.getProperty("UserAttributes")
- uservals.update({pname: params[pname]})
-
- if len(uservals.items()) > 0:
- prop.setupProperty(obj, "UserAttributes",
- PropertyGroupAttribute, uservals)
-
return obj
def CreateFrom(self, path, name='ToolBit'):
+ PathLog.track(name, path)
try:
data = Declaration(path)
- bit = Factory.CreateFromAttrs(data, name)
- bit.File = path
+ bit = Factory.CreateFromAttrs(data, name, path)
return bit
except (OSError, IOError) as e:
PathLog.error("%s not a valid tool file (%s)" % (path, e))
raise
- def Create(self, name='ToolBit', shapeFile=None):
+ def Create(self, name='ToolBit', shapeFile=None, path=None):
+ PathLog.track(name, shapeFile, path)
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name)
- obj.Proxy = ToolBit(obj, shapeFile)
+ obj.Proxy = ToolBit(obj, shapeFile, path)
return obj
diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py
index 3488bcc95d..3028bd7bb1 100644
--- a/src/Mod/Path/PathScripts/PathToolBitEdit.py
+++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py
@@ -20,12 +20,14 @@
# * *
# ***************************************************************************
+import FreeCAD
import FreeCADGui
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
-import PathScripts.PathSetupSheetGui as PathSetupSheetGui
+import PathScripts.PathPropertyEditor as PathPropertyEditor
import PathScripts.PathToolBit as PathToolBit
+import PathScripts.PathUtil as PathUtil
import os
import re
@@ -39,6 +41,31 @@ PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
+class _Delegate(QtGui.QStyledItemDelegate):
+ '''Handles the creation of an appropriate editing widget for a given property.'''
+ ObjectRole = QtCore.Qt.UserRole + 1
+ PropertyRole = QtCore.Qt.UserRole + 2
+ EditorRole = QtCore.Qt.UserRole + 3
+
+ def createEditor(self, parent, option, index):
+ editor = index.data(self.EditorRole)
+ if editor is None:
+ obj = index.data(self.ObjectRole)
+ prp = index.data(self.PropertyRole)
+ editor = PathPropertyEditor.Editor(obj, prp)
+ index.model().setData(index, editor, self.EditorRole)
+ return editor.widget(parent)
+
+ def setEditorData(self, widget, index):
+ # called to update the widget with the current data
+ index.data(self.EditorRole).setEditorData(widget)
+
+ def setModelData(self, widget, model, index):
+ # called to update the model with the data from the widget
+ editor = index.data(self.EditorRole)
+ editor.setModelData(widget)
+ index.model().setData(index, PathUtil.getPropertyValueString(editor.obj, editor.prop), QtCore.Qt.DisplayRole)
+
class ToolBitEditor(object):
'''UI and controller for editing a ToolBit.
@@ -84,32 +111,35 @@ class ToolBitEditor(object):
layout = self.form.bitParams.layout()
ui = FreeCADGui.UiLoader()
- nr = 0
# for all properties either assign them to existing labels and editors
# or create additional ones for them if not enough have already been
# created.
- for name in tool.PropertiesList:
- if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit:
- if nr < len(self.widgets):
- PathLog.debug("re-use row: {} [{}]".format(nr, name))
- label, qsb, editor = self.widgets[nr]
- label.setText(labelText(name))
- editor.attachTo(tool, name)
- label.show()
- qsb.show()
- else:
- qsb = ui.createWidget('Gui::QuantitySpinBox')
- editor = PathGui.QuantitySpinBox(qsb, tool, name)
- label = QtGui.QLabel(labelText(name))
- self.widgets.append((label, qsb, editor))
- PathLog.debug("create row: {} [{}]".format(nr, name))
- if nr >= layout.rowCount():
- layout.addRow(label, qsb)
- nr = nr + 1
+ usedRows = 0
+ for nr, name in enumerate(tool.Proxy.toolShapeProperties(tool)):
+ if nr < len(self.widgets):
+ PathLog.debug("re-use row: {} [{}]".format(nr, name))
+ label, qsb, editor = self.widgets[nr]
+ label.setText(labelText(name))
+ editor.attachTo(tool, name)
+ label.show()
+ qsb.show()
+ else:
+ qsb = ui.createWidget('Gui::QuantitySpinBox')
+ editor = PathGui.QuantitySpinBox(qsb, tool, name)
+ label = QtGui.QLabel(labelText(name))
+ self.widgets.append((label, qsb, editor))
+ PathLog.debug("create row: {} [{}] {}".format(nr, name, type(qsb)))
+ if hasattr(qsb, 'editingFinished'):
+ qsb.editingFinished.connect(self.updateTool)
+
+ if nr >= layout.rowCount():
+ layout.addRow(label, qsb)
+ usedRows = usedRows + 1
# hide all rows which aren't being used
- for i in range(nr, len(self.widgets)):
+ PathLog.track(usedRows, len(self.widgets))
+ for i in range(usedRows, len(self.widgets)):
label, qsb, editor = self.widgets[i]
label.hide()
qsb.hide()
@@ -124,98 +154,48 @@ class ToolBitEditor(object):
def setupAttributes(self, tool):
PathLog.track()
- self.proto = PathToolBit.AttributePrototype()
- self.props = sorted(self.proto.properties)
- self.delegate = PathSetupSheetGui.Delegate(self.form)
- self.model = QtGui.QStandardItemModel(len(self.props)-1, 3, self.form)
- self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value'])
- for i, name in enumerate(self.props):
- PathLog.debug("propname: %s " % name)
+ setup = True
+ if not hasattr(self, 'delegate'):
+ self.delegate = _Delegate(self.form.attrTree)
+ self.model = QtGui.QStandardItemModel(self.form.attrTree)
+ self.model.setHorizontalHeaderLabels(['Property', 'Value'])
+ else:
+ self.model.removeRows(0, self.model.rowCount())
+ setup = False
- prop = self.proto.getProperty(name)
- isset = hasattr(tool, name)
+ attributes = tool.Proxy.toolGroupsAndProperties(tool, False)
+ for name in attributes:
+ group = QtGui.QStandardItem()
+ group.setData(name, QtCore.Qt.EditRole)
+ group.setEditable(False)
+ for prop in attributes[name]:
+ label = QtGui.QStandardItem()
+ label.setData(prop, QtCore.Qt.EditRole)
+ label.setEditable(False)
- if isset:
- prop.setValue(getattr(tool, name))
+ value = QtGui.QStandardItem()
+ value.setData(PathUtil.getPropertyValueString(tool, prop), QtCore.Qt.DisplayRole)
+ value.setData(tool, _Delegate.ObjectRole)
+ value.setData(prop, _Delegate.PropertyRole)
- if name == "UserAttributes":
- continue
+ group.appendRow([label, value])
+ self.model.appendRow(group)
- else:
- self.model.setData(self.model.index(i, 0), isset,
- QtCore.Qt.EditRole)
- self.model.setData(self.model.index(i, 1), name,
- QtCore.Qt.EditRole)
- self.model.setData(self.model.index(i, 2), prop,
- PathSetupSheetGui.Delegate.PropertyRole)
- self.model.setData(self.model.index(i, 2),
- prop.displayString(),
- QtCore.Qt.DisplayRole)
-
- self.model.item(i, 0).setCheckable(True)
- self.model.item(i, 0).setText('')
- self.model.item(i, 1).setEditable(False)
- self.model.item(i, 1).setToolTip(prop.info)
- self.model.item(i, 2).setToolTip(prop.info)
-
- if isset:
- self.model.item(i, 0).setCheckState(QtCore.Qt.Checked)
- else:
- self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked)
- self.model.item(i, 1).setEnabled(False)
- self.model.item(i, 2).setEnabled(False)
-
- if hasattr(tool, "UserAttributes"):
- for key, value in tool.UserAttributes.items():
- PathLog.debug(key, value)
- c1 = QtGui.QStandardItem()
- c1.setCheckable(False)
- c1.setEditable(False)
- c1.setCheckState(QtCore.Qt.CheckState.Checked)
-
- c1.setText('')
- c2 = QtGui.QStandardItem(key)
- c2.setEditable(False)
- c3 = QtGui.QStandardItem(value)
- c3.setEditable(False)
-
- self.model.appendRow([c1, c2, c3])
-
- self.form.attrTable.setModel(self.model)
- self.form.attrTable.setItemDelegateForColumn(2, self.delegate)
- self.form.attrTable.resizeColumnsToContents()
- self.form.attrTable.verticalHeader().hide()
-
- self.model.dataChanged.connect(self.updateData)
-
- def updateData(self, topLeft, bottomRight):
- PathLog.track()
- if 0 == topLeft.column():
- isset = self.model.item(topLeft.row(),
- 0).checkState() == QtCore.Qt.Checked
- self.model.item(topLeft.row(), 1).setEnabled(isset)
- self.model.item(topLeft.row(), 2).setEnabled(isset)
+ if setup:
+ self.form.attrTree.setModel(self.model)
+ self.form.attrTree.setItemDelegateForColumn(1, self.delegate)
+ self.form.attrTree.expandAll()
+ self.form.attrTree.resizeColumnToContents(0)
+ self.form.attrTree.resizeColumnToContents(1)
+ #self.form.attrTree.collapseAll()
def accept(self):
PathLog.track()
self.refresh()
self.tool.Proxy.unloadBitBody(self.tool)
- # get the attributes
- for i, name in enumerate(self.props):
- PathLog.debug('in accept: {}'.format(name))
- prop = self.proto.getProperty(name)
- if self.model.item(i, 0) is not None:
- enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked
- if enabled and not prop.getValue() is None:
- prop.setupProperty(self.tool, name,
- PathToolBit.PropertyGroupAttribute,
- prop.getValue())
- elif hasattr(self.tool, name):
- self.tool.removeProperty(name)
-
def reject(self):
PathLog.track()
self.tool.Proxy.unloadBitBody(self.tool)
@@ -228,27 +208,45 @@ class ToolBitEditor(object):
for lbl, qsb, editor in self.widgets:
editor.updateSpinBox()
+ def _updateBitShape(self, shapePath):
+ # Only need to go through this exercise if the shape actually changed.
+ if self.tool.BitShape != shapePath:
+ # Before setting a new bitshape we need to make sure that none of
+ # editors fires an event and tries to access its old property, which
+ # might not exist anymore.
+ for lbl, qsb, editor in self.widgets:
+ editor.attachTo(self.tool, 'File')
+ self.tool.BitShape = shapePath
+ self.setupTool(self.tool)
+ self.form.toolName.setText(self.tool.Label)
+ if self.tool.BitBody and self.tool.BitBody.ViewObject:
+ if not self.tool.BitBody.ViewObject.Visibility:
+ self.tool.BitBody.ViewObject.Visibility = True
+ self.setupAttributes(self.tool)
+ return True
+ return False
+
def updateShape(self):
PathLog.track()
shapePath = str(self.form.shapePath.text())
# Only need to go through this exercise if the shape actually changed.
- if self.tool.BitShape != shapePath:
- self.tool.BitShape = shapePath
- self.setupTool(self.tool)
- self.form.toolName.setText(self.tool.Label)
-
+ if self._updateBitShape(shapePath):
for lbl, qsb, editor in self.widgets:
editor.updateSpinBox()
def updateTool(self):
PathLog.track()
- self.tool.Label = str(self.form.toolName.text())
- self.tool.BitShape = str(self.form.shapePath.text())
+
+ label = str(self.form.toolName.text())
+ shape = str(self.form.shapePath.text())
+ if self.tool.Label != label:
+ self.tool.Label = label
+ self._updateBitShape(shape)
for lbl, qsb, editor in self.widgets:
editor.updateProperty()
- # self.tool.Proxy._updateBitShape(self.tool)
+ self.tool.Proxy._updateBitShape(self.tool)
def refresh(self):
PathLog.track()
@@ -262,10 +260,7 @@ class ToolBitEditor(object):
path = self.tool.BitShape
if not path:
path = PathPreferences.lastPathToolShape()
- foo = QtGui.QFileDialog.getOpenFileName(self.form,
- "Path - Tool Shape",
- path,
- "*.fcstd")
+ foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Shape", path, "*.fcstd")
if foo and foo[0]:
PathPreferences.setLastPathToolShape(os.path.dirname(foo[0]))
self.form.shapePath.setText(foo[0])
diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py
index 1fe1df5370..1bf56aea68 100644
--- a/src/Mod/Path/PathScripts/PathToolBitGui.py
+++ b/src/Mod/Path/PathScripts/PathToolBitGui.py
@@ -170,13 +170,13 @@ class TaskPanel:
class ToolBitGuiFactory(PathToolBit.ToolBitFactory):
- def Create(self, name='ToolBit', shapeFile=None):
+ def Create(self, name='ToolBit', shapeFile=None, path=None):
'''Create(name = 'ToolBit') ... creates a new tool bit.
It is assumed the tool will be edited immediately so the internal bit body is still attached.'''
- FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit',
- 'Create ToolBit'))
- tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile)
+ PathLog.track(name, shapeFile, path)
+ FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit'))
+ tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile, path)
PathIconViewProvider.Attach(tool.ViewObject, name)
FreeCAD.ActiveDocument.commitTransaction()
return tool
@@ -247,7 +247,8 @@ def GetToolShapeFile(parent=None):
location, '*.fcstd')
if fname and fname[0]:
if fname != location:
- PathPreferences.setLastPathToolShape(location)
+ newloc = os.path.dirname(fname[0])
+ PathPreferences.setLastPathToolShape(newloc)
return fname[0]
else:
return None
diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py
index 33e8fb8e39..621ad0e996 100644
--- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py
+++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py
@@ -24,23 +24,25 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathLog as PathLog
import PathScripts.PathPreferences as PathPreferences
import PathScripts.PathToolBit as PathToolBit
-import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolBitEdit as PathToolBitEdit
+import PathScripts.PathToolBitGui as PathToolBitGui
import PathScripts.PathToolControllerGui as PathToolControllerGui
import PathScripts.PathUtilsGui as PathUtilsGui
-from PySide import QtCore, QtGui
import PySide
+import glob
import json
import os
-import glob
-import uuid as UUID
-from functools import partial
import shutil
+import uuid as UUID
-# PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+from functools import partial
+
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# PathLog.trackModule(PathLog.thisModule())
_UuidRole = PySide.QtCore.Qt.UserRole + 1
@@ -50,6 +52,75 @@ _PathRole = PySide.QtCore.Qt.UserRole + 2
def translate(context, text, disambig=None):
return PySide.QtCore.QCoreApplication.translate(context, text, disambig)
+def checkWorkingDir():
+ # users shouldn't use the example toolbits and libraries.
+ # working directory should be writable
+ PathLog.track()
+
+ workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary())
+ defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath())
+
+ PathLog.debug('workingdir: {} defaultdir: {}'.format(workingdir, defaultdir))
+
+ dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK))
+
+ if dirOK():
+ return True
+
+ qm = PySide.QtGui.QMessageBox
+ ret = qm.question(None,'', "Toolbit working directory not set up. Do that now?", qm.Yes | qm.No)
+
+ if ret == qm.No:
+ return False
+
+ msg = translate("Path", "Choose a writable location for your toolbits", None)
+ while not dirOK():
+ workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(None, msg,
+ PathPreferences.filePath())
+
+ if workingdir[-8:] == os.path.sep + 'Library':
+ workingdir = workingdir[:-8] # trim off trailing /Library if user chose it
+
+ PathPreferences.setLastPathToolLibrary("{}{}Library".format(workingdir, os.path.sep))
+ PathPreferences.setLastPathToolBit("{}{}Bit".format(workingdir, os.path.sep))
+ PathLog.debug('setting workingdir to: {}'.format(workingdir))
+
+ subdirlist = ['Bit', 'Library', 'Shape']
+ mode = 0o777
+ for dir in subdirlist:
+ subdir = "{}{}{}".format(workingdir, os.path.sep, dir)
+ if os.path.exists(subdir):
+ subdirlist.remove(dir)
+
+ if len(subdirlist) >= 1:
+ needed = ', '.join([str(d) for d in subdirlist])
+ qm = PySide.QtGui.QMessageBox
+ ret = qm.question(None,'', "Toolbit Working directory {} needs these sudirectories:\n {} \n Create them?".format(workingdir, needed), qm.Yes | qm.No)
+
+ if ret == qm.No:
+ return False
+ else:
+ for dir in subdirlist:
+ subdir = "{}{}{}".format(workingdir, os.path.sep, dir)
+ os.mkdir(subdir, mode)
+ if dir != 'Shape':
+ qm = PySide.QtGui.QMessageBox
+ ret = qm.question(None,'', "Copy example files to new {} directory?".format(dir), qm.Yes | qm.No)
+ if ret == qm.Yes:
+ src="{}{}{}".format(defaultdir, os.path.sep, dir)
+ src_files = os.listdir(src)
+ for file_name in src_files:
+ full_file_name = os.path.join(src, file_name)
+ if os.path.isfile(full_file_name):
+ shutil.copy(full_file_name, subdir)
+
+
+ # if no library is set, choose the first one in the Library directory
+ if PathPreferences.lastFileToolLibrary() is None:
+ libFiles = [f for f in glob.glob(PathPreferences.lastPathToolLibrary() + os.path.sep + '*.fctl')]
+ PathPreferences.setLastFileToolLibrary(libFiles[0])
+
+ return True
class _TableView(PySide.QtGui.QTableView):
'''Subclass of QTableView to support rearrange and copying of ToolBits'''
@@ -141,7 +212,7 @@ class ModelFactory(object):
for toolBit in library['tools']:
try:
nr = toolBit['nr']
- bit = PathToolBit.findBit(toolBit['path'])
+ bit = PathToolBit.findToolBit(toolBit['path'], path)
if bit:
PathLog.track(bit)
tool = PathToolBit.Declaration(bit)
@@ -203,15 +274,15 @@ class ModelFactory(object):
path = PathPreferences.lastPathToolLibrary()
if os.path.isdir(path): # opening all tables in a directory
- libFiles = [f for f in glob.glob(path + '/*.fctl')]
+ libFiles = [f for f in glob.glob(path + os.path.sep + '*.fctl')]
libFiles.sort()
for libFile in libFiles:
loc, fnlong = os.path.split(libFile)
fn, ext = os.path.splitext(fnlong)
- libItem = QtGui.QStandardItem(fn)
+ libItem = PySide.QtGui.QStandardItem(fn)
libItem.setToolTip(loc)
libItem.setData(libFile, _PathRole)
- libItem.setIcon(QtGui.QPixmap(':/icons/Path_ToolTable.svg'))
+ libItem.setIcon(PySide.QtGui.QPixmap(':/icons/Path_ToolTable.svg'))
model.appendRow(libItem)
PathLog.debug('model rows: {}'.format(model.rowCount()))
@@ -241,6 +312,7 @@ class ToolBitSelector(object):
'''Controller for displaying a library and creating ToolControllers'''
def __init__(self):
+ checkWorkingDir()
self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui')
self.factory = ModelFactory()
self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()))
@@ -250,20 +322,20 @@ class ToolBitSelector(object):
def columnNames(self):
return ['#', 'Tool']
- def curLib(self):
+ def currentLibrary(self, shortNameOnly):
libfile = PathPreferences.lastFileToolLibrary()
if libfile is None or libfile == "":
return ""
- else:
- libfile = os.path.split(PathPreferences.lastFileToolLibrary())[1]
- libfile = os.path.splitext(libfile)[0]
+ elif shortNameOnly:
+ return os.path.splitext(os.path.basename(libfile))[0]
return libfile
def loadData(self):
PathLog.track()
self.toolModel.clear()
self.toolModel.setHorizontalHeaderLabels(self.columnNames())
- self.form.lblLibrary.setText(self.curLib())
+ self.form.lblLibrary.setText(self.currentLibrary(True))
+ self.form.lblLibrary.setToolTip(self.currentLibrary(False))
self.factory.libraryOpen(self.toolModel)
self.toolModel.takeColumn(3)
self.toolModel.takeColumn(2)
@@ -271,7 +343,6 @@ class ToolBitSelector(object):
def setupUI(self):
PathLog.track()
self.loadData()
-
self.form.tools.setModel(self.toolModel)
self.form.tools.selectionModel().selectionChanged.connect(self.enableButtons)
self.form.tools.doubleClicked.connect(partial(self.selectedOrAllToolControllers))
@@ -340,7 +411,7 @@ class ToolBitSelector(object):
def open(self, path=None):
''' load library stored in path and bring up ui'''
- docs = FreeCADGui.getMainWindow().findChildren(QtGui.QDockWidget)
+ docs = FreeCADGui.getMainWindow().findChildren(PySide.QtGui.QDockWidget)
for doc in docs:
if doc.objectName() == "ToolSelector":
if doc.isVisible():
@@ -351,7 +422,7 @@ class ToolBitSelector(object):
return
mw = FreeCADGui.getMainWindow()
- mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.form,
+ mw.addDockWidget(PySide.QtCore.Qt.RightDockWidgetArea, self.form,
PySide.QtCore.Qt.Orientation.Vertical)
@@ -361,9 +432,7 @@ class ToolBitLibrary(object):
def __init__(self):
PathLog.track()
- if not self.checkWorkingDir():
- return
-
+ checkWorkingDir()
self.factory = ModelFactory()
self.temptool = None
self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()))
@@ -375,55 +444,6 @@ class ToolBitLibrary(object):
self.setupUI()
self.title = self.form.windowTitle()
- def checkWorkingDir(self):
- # users shouldn't use the example toolbits and libraries.
- # working directory should be writable
- PathLog.track()
-
- workingdir = os.path.dirname(PathPreferences.lastPathToolLibrary())
- defaultdir = os.path.dirname(PathPreferences.pathDefaultToolsPath())
-
- dirOK = lambda : workingdir != defaultdir and (os.access(workingdir, os.W_OK))
-
- if dirOK():
- return True
-
- qm = PySide.QtGui.QMessageBox
- ret = qm.question(None,'', "Toolbit working directory not set up. Do that now?", qm.Yes | qm.No)
-
- if ret == qm.No:
- return False
-
- msg = translate("Path", "Choose a writable location for your toolbits", None)
- while not dirOK():
- workingdir = PySide.QtGui.QFileDialog.getExistingDirectory(None, msg,
- PathPreferences.filePath())
-
- PathPreferences.setLastPathToolLibrary("{}/Library".format(workingdir))
-
- subdirlist = ['Bit', 'Library', 'Shape']
- mode = 0o777
- for dir in subdirlist:
- subdir = "{}/{}".format(workingdir, dir)
- if not os.path.exists(subdir):
- qm = PySide.QtGui.QMessageBox
- ret = qm.question(None,'', "Toolbit Working directory {} should contain a '{}' subdirectory. Create it?".format(workingdir, dir), qm.Yes | qm.No)
-
- if ret == qm.Yes:
- os.mkdir(subdir, mode)
- qm = PySide.QtGui.QMessageBox
- ret = qm.question(None,'', "Copy example files to new {} directory?".format(dir), qm.Yes | qm.No)
- if ret == qm.Yes:
- src="{}/{}".format(defaultdir, dir)
- src_files = os.listdir(src)
- for file_name in src_files:
- full_file_name = os.path.join(src, file_name)
- if os.path.isfile(full_file_name):
- shutil.copy(full_file_name, subdir)
-
- return True
-
-
def toolBitNew(self):
PathLog.track()
@@ -439,7 +459,7 @@ class ToolBitLibrary(object):
# Parse out the name of the file and write the structure
loc, fil = os.path.split(filename)
fname = os.path.splitext(fil)[0]
- fullpath = "{}/{}.fctb".format(loc, fname)
+ fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname)
PathLog.debug(fullpath)
self.temptool = PathToolBit.ToolBitFactory().Create(name=fname)
@@ -464,7 +484,7 @@ class ToolBitLibrary(object):
loc, fil = os.path.split(f)
fname = os.path.splitext(fil)[0]
- fullpath = "{}/{}.fctb".format(loc, fname)
+ fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname)
self.factory.newTool(self.toolModel, fullpath)
@@ -557,8 +577,8 @@ class ToolBitLibrary(object):
self.temptool = PathToolBit.ToolBitFactory().CreateFrom(tbpath, 'temptool')
self.editor = PathToolBitEdit.ToolBitEditor(self.temptool, self.form.toolTableGroup, loadBitBody=False)
- QBtn = QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
- buttonBox = QtGui.QDialogButtonBox(QBtn)
+ QBtn = PySide.QtGui.QDialogButtonBox.Ok | PySide.QtGui.QDialogButtonBox.Cancel
+ buttonBox = PySide.QtGui.QDialogButtonBox(QBtn)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
@@ -604,8 +624,9 @@ class ToolBitLibrary(object):
else:
tools.append({'nr': toolNr, 'path': PathToolBit.findRelativePathTool(toolPath)})
- with open(self.path, 'w') as fp:
- json.dump(library, fp, sort_keys=True, indent=2)
+ if self.path is not None:
+ with open(self.path, 'w') as fp:
+ json.dump(library, fp, sort_keys=True, indent=2)
def libraryOk(self):
self.librarySave()
@@ -652,7 +673,7 @@ class ToolBitLibrary(object):
if curIndex:
sm = self.form.TableList.selectionModel()
- sm.select(curIndex, QtCore.QItemSelectionModel.Select)
+ sm.select(curIndex, PySide.QtCore.QItemSelectionModel.Select)
self.toolTableView.setUpdatesEnabled(True)
self.form.TableList.setUpdatesEnabled(True)
diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py
index 199d5cdf22..3f8405ccd3 100644
--- a/src/Mod/Path/PathScripts/PathToolController.py
+++ b/src/Mod/Path/PathScripts/PathToolController.py
@@ -250,7 +250,8 @@ def Create(name='TC: Default Tool', tool=None, toolNumber=1, assignViewProvider=
if tool.ViewObject:
tool.ViewObject.Visibility = False
- obj.Tool = tool
+ if tool:
+ obj.Tool = tool
obj.ToolNumber = toolNumber
return obj
@@ -260,7 +261,7 @@ def FromTemplate(template, assignViewProvider=True):
PathLog.track()
name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label)
- obj = Create(name, assignViewProvider=True)
+ obj = Create(name, tool=False, assignViewProvider=True)
obj.Proxy.setFromTemplate(obj, template)
return obj
diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py
index 33e735a08d..905888026e 100644
--- a/src/Mod/Path/PathScripts/PathToolControllerGui.py
+++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py
@@ -22,6 +22,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts
import PathScripts.PathGui as PathGui
import PathScripts.PathLog as PathLog
diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py
index 40e59a78af..db43b466e5 100644
--- a/src/Mod/Path/PathScripts/PathUtil.py
+++ b/src/Mod/Path/PathScripts/PathUtil.py
@@ -71,6 +71,11 @@ def getPropertyValueString(obj, prop):
def setProperty(obj, prop, value):
'''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.'''
o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable
+ if not attr is None and type(value) == str:
+ if type(attr) == int:
+ value = int(value, 0)
+ elif type(attr) == bool:
+ value = value.lower() in ['true', '1', 'yes', 'ok']
if o and name:
setattr(o, name, value)
diff --git a/src/Mod/Path/PathScripts/PathUtilsGui.py b/src/Mod/Path/PathScripts/PathUtilsGui.py
index da33987017..a5df387eb3 100644
--- a/src/Mod/Path/PathScripts/PathUtilsGui.py
+++ b/src/Mod/Path/PathScripts/PathUtilsGui.py
@@ -21,6 +21,7 @@
# ***************************************************************************
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts
import PathScripts.PathJobCmd as PathJobCmd
import PathScripts.PathUtils as PathUtils
diff --git a/src/Mod/Path/PathScripts/PathVcarveGui.py b/src/Mod/Path/PathScripts/PathVcarveGui.py
index e77dab4b7f..286cce9b28 100644
--- a/src/Mod/Path/PathScripts/PathVcarveGui.py
+++ b/src/Mod/Path/PathScripts/PathVcarveGui.py
@@ -23,6 +23,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathVcarve as PathVcarve
import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index 07958536fd..089e15256a 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -24,6 +24,7 @@
import FreeCAD
import FreeCADGui
+import PathGui as PGui # ensure Path/Gui/Resources are loaded
import PathScripts.PathWaterline as PathWaterline
import PathScripts.PathGui as PathGui
import PathScripts.PathOpGui as PathOpGui
diff --git a/src/Mod/Path/PathTests/TestPathPropertyBag.py b/src/Mod/Path/PathTests/TestPathPropertyBag.py
new file mode 100644
index 0000000000..974ca3bd6c
--- /dev/null
+++ b/src/Mod/Path/PathTests/TestPathPropertyBag.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * Copyright (c) 2021 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 *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import PathScripts.PathPropertyBag as PathPropertyBag
+import PathTests.PathTestUtils as PathTestUtils
+
+class TestPathPropertyBag(PathTestUtils.PathTestBase):
+
+ def setUp(self):
+ self.doc = FreeCAD.newDocument('test-property-bag')
+
+ def tearDown(self):
+ FreeCAD.closeDocument(self.doc.Name)
+
+ def test00(self):
+ '''basic PropertyBag creation and access test'''
+ bag = PathPropertyBag.Create()
+ self.assertTrue(hasattr(bag, 'Proxy'))
+ self.assertEqual(bag.Proxy.getCustomProperties(), [])
+ self.assertEqual(bag.CustomPropertyGroups, [])
+
+ def test01(self):
+ '''adding properties to a PropertyBag is tracked properly'''
+ bag = PathPropertyBag.Create()
+ proxy = bag.Proxy
+ proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description')
+ self.assertTrue(hasattr(bag, 'Title'))
+ bag.Title = 'Madame'
+ self.assertEqual(bag.Title, 'Madame')
+ self.assertEqual(bag.Proxy.getCustomProperties(), ['Title'])
+ self.assertEqual(bag.CustomPropertyGroups, ['Address'])
+
+ def test02(self):
+ '''refreshCustomPropertyGroups deletes empty groups'''
+ bag = PathPropertyBag.Create()
+ proxy = bag.Proxy
+ proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description')
+ bag.Title = 'Madame'
+ bag.removeProperty('Title')
+ proxy.refreshCustomPropertyGroups()
+ self.assertEqual(bag.Proxy.getCustomProperties(), [])
+ self.assertEqual(bag.CustomPropertyGroups, [])
+
+ def test03(self):
+ '''refreshCustomPropertyGroups does not delete non-empty groups'''
+ bag = PathPropertyBag.Create()
+ proxy = bag.Proxy
+ proxy.addCustomProperty('App::PropertyString', 'Title', 'Address', 'Some description')
+ proxy.addCustomProperty('App::PropertyString', 'Gender', 'Attributes')
+ bag.Title = 'Madame'
+ bag.Gender = 'Female'
+ bag.removeProperty('Gender')
+ proxy.refreshCustomPropertyGroups()
+ self.assertEqual(bag.Proxy.getCustomProperties(), ['Title'])
+ self.assertEqual(bag.CustomPropertyGroups, ['Address'])
+
diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py
index dd82c7fc63..7a684b3697 100644
--- a/src/Mod/Path/PathTests/TestPathToolBit.py
+++ b/src/Mod/Path/PathTests/TestPathToolBit.py
@@ -22,37 +22,143 @@
import PathScripts.PathToolBit as PathToolBit
import PathTests.PathTestUtils as PathTestUtils
+import glob
+import os
+TestToolDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Tools')
+TestInvalidDir = os.path.join(TestToolDir, 'some', 'silly', 'path', 'that', 'should', 'not', 'exist')
+
+TestToolBitName = 'test-path-tool-bit-bit-00.fctb'
+TestToolShapeName = 'test-path-tool-bit-shape-00.fcstd'
+TestToolLibraryName = 'test-path-tool-bit-library-00.fctl'
+
+def testToolShape(path = TestToolDir, name = TestToolShapeName):
+ return os.path.join(path, 'Shape', name)
+
+def testToolBit(path = TestToolDir, name = TestToolBitName):
+ return os.path.join(path, 'Bit', name)
+
+def testToolLibrary(path = TestToolDir, name = TestToolLibraryName):
+ return os.path.join(path, 'Library', name)
+
+def printTree(path, indent):
+ print("{} {}".format(indent, os.path.basename(path)))
+ if os.path.isdir(path):
+ if os.path.basename(path).startswith('__'):
+ print("{} ...".format(indent))
+ else:
+ for foo in sorted(glob.glob(os.path.join(path, '*'))):
+ printTree(foo, "{} ".format(indent))
class TestPathToolBit(PathTestUtils.PathTestBase):
- def test00(self):
- '''Find a tool shapee from file name'''
+ def test(self):
+ '''Log test setup directory structure'''
+ # Enable this test if there are errors showing up in the build system with the
+ # paths that work OK locally. It'll print out the directory tree, and if it
+ # doesn't look right you know where to look for it
+ print()
+ print("realpath : {}".format(os.path.realpath(__file__)))
+ print(" Tools : {}".format(TestToolDir))
+ print(" dir : {}".format(os.path.dirname(os.path.realpath(__file__))))
+ printTree(os.path.dirname(os.path.realpath(__file__)), " :")
- path = PathToolBit.findShape('endmill.fcstd')
+ def test00(self):
+ '''Find a tool shape from file name'''
+ path = PathToolBit.findToolShape('endmill.fcstd')
self.assertIsNot(path, None)
self.assertNotEqual(path, 'endmill.fcstd')
- def test01(self):
- '''Find a tool shapee from an invalid absolute path.'''
- path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd')
+ def test01(self):
+ '''Not find a relative path shape if not stored in default location'''
+ path = PathToolBit.findToolShape(TestToolShapeName)
+ self.assertIsNone(path)
+
+
+ def test02(self):
+ '''Find a relative path shape if it's local to a bit path'''
+ path = PathToolBit.findToolShape(TestToolShapeName, testToolBit())
self.assertIsNot(path, None)
- self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd')
+ self.assertEqual(path, testToolShape())
+
+
+ def test03(self):
+ '''Not find a tool shape from an invalid absolute path.'''
+ path = PathToolBit.findToolShape(testToolShape(TestInvalidDir))
+ self.assertIsNone(path)
+
+
+ def test04(self):
+ '''Find a tool shape from a valid absolute path.'''
+ path = PathToolBit.findToolShape(testToolShape())
+ self.assertIsNot(path, None)
+ self.assertEqual(path, testToolShape())
def test10(self):
- '''find the relative path of a tool bit'''
- shape = 'endmill.fcstd'
- path = PathToolBit.findShape(shape)
+ '''Find a tool bit from file name'''
+ path = PathToolBit.findToolBit('5mm_Endmill.fctb')
self.assertIsNot(path, None)
- self.assertGreater(len(path), len(shape))
- rel = PathToolBit.findRelativePathShape(path)
- self.assertEqual(rel, shape)
+ self.assertNotEqual(path, '5mm_Endmill.fctb')
+
def test11(self):
- '''store full path if relative path isn't found'''
- path = '/this/is/unlikely/a/valid/path/v-bit.fcstd'
- rel = PathToolBit.findRelativePathShape(path)
- self.assertEqual(rel, path)
+ '''Not find a relative path bit if not stored in default location'''
+ path = PathToolBit.findToolBit(TestToolBitName)
+ self.assertIsNone(path)
+
+
+ def test12(self):
+ '''Find a relative path bit if it's local to a library path'''
+ path = PathToolBit.findToolBit(TestToolBitName, testToolLibrary())
+ self.assertIsNot(path, None)
+ self.assertEqual(path, testToolBit())
+
+
+ def test13(self):
+ '''Not find a tool bit from an invalid absolute path.'''
+ path = PathToolBit.findToolBit(testToolBit(TestInvalidDir))
+ self.assertIsNone(path)
+
+
+ def test14(self):
+ '''Find a tool bit from a valid absolute path.'''
+ path = PathToolBit.findToolBit(testToolBit())
+ self.assertIsNot(path, None)
+ self.assertEqual(path, testToolBit())
+
+
+
+ def test20(self):
+ '''Find a tool library from file name'''
+ path = PathToolBit.findToolLibrary('Default.fctl')
+ self.assertIsNot(path, None)
+ self.assertNotEqual(path, 'Default.fctl')
+
+
+ def test21(self):
+ '''Not find a relative path library if not stored in default location'''
+ path = PathToolBit.findToolLibrary(TestToolLibraryName)
+ self.assertIsNone(path)
+
+
+ def test22(self):
+ '''[skipped] Find a relative path library if it's local to '''
+ # this is not a valid test for libraries because t
+ self.assertTrue(True)
+
+
+ def test23(self):
+ '''Not find a tool library from an invalid absolute path.'''
+ path = PathToolBit.findToolLibrary(testToolLibrary(TestInvalidDir))
+ self.assertIsNone(path)
+
+
+ def test24(self):
+ '''Find a tool library from a valid absolute path.'''
+ path = PathToolBit.findToolBit(testToolBit())
+ self.assertIsNot(path, None)
+ self.assertEqual(path, testToolBit())
+
diff --git a/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb b/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb
new file mode 100644
index 0000000000..7cc72ba33c
--- /dev/null
+++ b/src/Mod/Path/PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb
@@ -0,0 +1,12 @@
+{
+ "version": 2,
+ "name": "5mm Endmill",
+ "shape": "endmill.fcstd",
+ "parameter": {
+ "CuttingEdgeHeight": "30.0000 mm",
+ "Diameter": "5.0000 mm",
+ "Length": "50.0000 mm",
+ "ShankDiameter": "3.0000 mm"
+ },
+ "attribute": {}
+}
diff --git a/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl b/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl
new file mode 100644
index 0000000000..60de98e08e
--- /dev/null
+++ b/src/Mod/Path/PathTests/Tools/Library/test-path-tool-bit-library-00.fctl
@@ -0,0 +1,41 @@
+{
+ "tools": [
+ {
+ "nr": 1,
+ "path": "5mm_Endmill.fctb"
+ },
+ {
+ "nr": 2,
+ "path": "5mm_Drill.fctb"
+ },
+ {
+ "nr": 3,
+ "path": "6mm_Ball_End.fctb"
+ },
+ {
+ "nr": 4,
+ "path": "6mm_Bullnose.fctb"
+ },
+ {
+ "nr": 5,
+ "path": "60degree_Vbit.fctb"
+ },
+ {
+ "nr": 6,
+ "path": "45degree_chamfer.fctb"
+ },
+ {
+ "nr": 7,
+ "path": "slittingsaw.fctb"
+ },
+ {
+ "nr": 8,
+ "path": "probe.fctb"
+ },
+ {
+ "nr": 9,
+ "path": "5mm-thread-cutter.fctb"
+ }
+ ],
+ "version": 1
+}
diff --git a/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd b/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd
new file mode 100644
index 0000000000..5b5a76dc41
Binary files /dev/null and b/src/Mod/Path/PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd differ
diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py
index 0adebedfc6..0be621b040 100644
--- a/src/Mod/Path/TestPathApp.py
+++ b/src/Mod/Path/TestPathApp.py
@@ -24,6 +24,7 @@ import TestApp
from PathTests.TestPathLog import TestPathLog
from PathTests.TestPathPreferences import TestPathPreferences
+from PathTests.TestPathPropertyBag import TestPathPropertyBag
from PathTests.TestPathCore import TestPathCore
#from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathGeom import TestPathGeom
@@ -66,4 +67,5 @@ False if TestPathToolBit.__name__ else True
False if TestPathVoronoi.__name__ else True
False if TestPathThreadMilling.__name__ else True
False if TestPathVcarve.__name__ else True
+False if TestPathPropertyBag.__name__ else True
diff --git a/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb b/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb
index 6c1231ed0f..77e53e523c 100644
--- a/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb
+++ b/src/Mod/Path/Tools/Bit/45degree_chamfer.fctb
@@ -11,4 +11,4 @@
"ShankDiameter": "6.3500 mm"
},
"attribute": {}
-}
\ No newline at end of file
+}
diff --git a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb
index 265978053b..2a5f0253cd 100644
--- a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb
+++ b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb
@@ -1,14 +1,14 @@
{
"version": 2,
- "name": "3mm-thread-cutter",
+ "name": "5mm-thread-cutter",
"shape": "thread-mill.fcstd",
"parameter": {
"Crest": "0.10 mm",
"Diameter": "5.00 mm",
"Length": "50.00 mm",
"NeckDiameter": "3.00 mm",
- "NeckHeight": "20.00 mm",
+ "NeckLength": "20.00 mm",
"ShankDiameter": "5.00 mm"
},
"attribute": {}
-}
\ No newline at end of file
+}
diff --git a/src/Mod/Path/Tools/Bit/probe.fctb b/src/Mod/Path/Tools/Bit/probe.fctb
index b92828e7ac..31fa354ff6 100644
--- a/src/Mod/Path/Tools/Bit/probe.fctb
+++ b/src/Mod/Path/Tools/Bit/probe.fctb
@@ -1,6 +1,6 @@
{
"version": 2,
- "name": "Probe004",
+ "name": "Probe",
"shape": "probe.fcstd",
"parameter": {
"Diameter": "6.0000 mm",
diff --git a/src/Mod/Path/Tools/Bit/slittingsaw.fctb b/src/Mod/Path/Tools/Bit/slittingsaw.fctb
index e9d33fe571..5b63c060ed 100644
--- a/src/Mod/Path/Tools/Bit/slittingsaw.fctb
+++ b/src/Mod/Path/Tools/Bit/slittingsaw.fctb
@@ -4,8 +4,8 @@
"shape": "slittingsaw.fcstd",
"parameter": {
"BladeThickness": "3.0000 mm",
- "BoltHeight": "3.0000 mm",
- "BoltWidth": "8.0000 mm",
+ "CapHeight": "3.0000 mm",
+ "CapDiameter": "8.0000 mm",
"Diameter": "76.2000 mm",
"Length": "50.0000 mm",
"ShankDiameter": "19.0500 mm"
diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md
index cdaed5a5b4..8f46b421bc 100644
--- a/src/Mod/Path/Tools/README.md
+++ b/src/Mod/Path/Tools/README.md
@@ -1,6 +1,6 @@
# Tools
-Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template.
+Each tool is stored as a JSON file which has the shape's path and values for all attributes of the shape.
It also includes all additional parameters and their values.
Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand,
@@ -8,10 +8,10 @@ storing each tool as a `*.fcstd` file requires more space and does not allow for
extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but
practically impossible for `*.fcstd` files.
-When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according
-to the values from the JSON file. All additional parameters are created as properties on the object. This provides the
-the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms (and
-potentially simulation).
+When a tool is instantiated in a job the PDN body is created from the shape and the attributes and constraints are set
+according to the values from the JSON file. All additional parameters are created as properties on the object. This
+provides the the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced
+algorithms (and potentially simulation).
# Tool Libraries
@@ -55,33 +55,34 @@ TechDraw's templates.
## How to create a new tool
1. Set the tool's Label, this will show up in the object tree
-1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template,
+1. Select a tool shape from the existing shape files. If your tool doesn't exist, you'll have to create a new shape,
see below for details.
-1. Each template has its own set of parameters, fill them with the tool's values.
+1. Each tool bit shape has its own set of parameters, fill them with the tool's values.
1. Select additional parameters
1. Save the tool under path/file that makes sense to you
## How to create a new tool bit Shape
-A tool bit template represents the physical shape of a tool. It does not completely describe the bit - for that some
-additional parameters are needed which will be added when an actual bit is parametrized from the template.
+The shape file for a tool bit is expected to contain a PD body which represents the tool as a 3d solid. The PD body
+should be parametric based on a a PropertyBag object so that, when the properties of the PropertyBag are changed the
+solid is updated to the correct representation.
1. Create a new FreeCAD document
1. Open the `PartDesign` workbench, create a body and give the body a label you want to show up in the bit selection.
-1. Create a sketch in the XZ plane and draw half the profile of the bit.
- * Put the top center of the bit on the origin (0,0)
-1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint
- * The name is the label of the input field
- * Names are split at CamelCase boundaries into words in the edit dialog
- * Use a `;` in the name to add help text which will show up as the entry fields tool tip
- * If the tool is used by legacy ops it should at least have one constraint called `Diameter`
- * Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle`
-1. Any unnamed constraint will not be editable for a specific tool
-1. Once the sketch is fully constrained, close the sketch
-1. Rotate the sketch around the z-axis
+1. Open the Path workbench and (with the PD body selected) create a PropertyBag,
+ menu 'Path' -> 'Utils' -> 'Property Bag'
+ * this creates a PropertyBag object inside the Body (assuming it was selected)
+ * add properties to which define the tool bit's shape and put those into the group 'Shape'
+ * add any other properties to the bag which might be useful for the tool bit
+1. Construct the body of the tool bit and assign experssions referencing properties from the PropertyBag (in the
+ `Shape` Group) for all constraints.
+ * Position the tip of the tool bit on the origin (0,0)
1. Save the document as a new file in the Shape directory
* Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in
FreeCAD's preferences.
* Also make sure to switch to _Front View_ and _Fit content to screen_
- * Whatever you see when saving the document will end up being the visual representation of the template
+ * Whatever you see when saving the document will end up being the visual representation of tool bits with this shape
+
+Not that 'Shape' is the only property group which has special meaning for tool bits. All other property groups are
+copied verbatim to the ToolBit object when one is created.
diff --git a/src/Mod/Path/Tools/Shape/ballend.fcstd b/src/Mod/Path/Tools/Shape/ballend.fcstd
index c3c5ee8b25..14b2a65efa 100644
Binary files a/src/Mod/Path/Tools/Shape/ballend.fcstd and b/src/Mod/Path/Tools/Shape/ballend.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/bullnose.fcstd b/src/Mod/Path/Tools/Shape/bullnose.fcstd
index c35ae78d81..63a50c4969 100644
Binary files a/src/Mod/Path/Tools/Shape/bullnose.fcstd and b/src/Mod/Path/Tools/Shape/bullnose.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/chamfer.fcstd b/src/Mod/Path/Tools/Shape/chamfer.fcstd
index 3470b4a3c6..59c7cb1a3a 100644
Binary files a/src/Mod/Path/Tools/Shape/chamfer.fcstd and b/src/Mod/Path/Tools/Shape/chamfer.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/drill.fcstd b/src/Mod/Path/Tools/Shape/drill.fcstd
index aa2a626d02..da1e277778 100644
Binary files a/src/Mod/Path/Tools/Shape/drill.fcstd and b/src/Mod/Path/Tools/Shape/drill.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/endmill.fcstd b/src/Mod/Path/Tools/Shape/endmill.fcstd
index 98888db1e3..b3b1ae18a4 100644
Binary files a/src/Mod/Path/Tools/Shape/endmill.fcstd and b/src/Mod/Path/Tools/Shape/endmill.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/probe.fcstd b/src/Mod/Path/Tools/Shape/probe.fcstd
index 5f853f415b..b4f20fa949 100644
Binary files a/src/Mod/Path/Tools/Shape/probe.fcstd and b/src/Mod/Path/Tools/Shape/probe.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/slittingsaw.fcstd b/src/Mod/Path/Tools/Shape/slittingsaw.fcstd
index 81d3d7f3d9..6694d911f7 100644
Binary files a/src/Mod/Path/Tools/Shape/slittingsaw.fcstd and b/src/Mod/Path/Tools/Shape/slittingsaw.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/thread-mill.fcstd b/src/Mod/Path/Tools/Shape/thread-mill.fcstd
index d97e56241f..c69e915396 100644
Binary files a/src/Mod/Path/Tools/Shape/thread-mill.fcstd and b/src/Mod/Path/Tools/Shape/thread-mill.fcstd differ
diff --git a/src/Mod/Path/Tools/Shape/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd
index 3168e5fe8d..51db09d7c7 100644
Binary files a/src/Mod/Path/Tools/Shape/v-bit.fcstd and b/src/Mod/Path/Tools/Shape/v-bit.fcstd differ
diff --git a/src/Mod/Path/Tools/toolbit-attributes.py b/src/Mod/Path/Tools/toolbit-attributes.py
new file mode 100755
index 0000000000..2f04ed6a58
--- /dev/null
+++ b/src/Mod/Path/Tools/toolbit-attributes.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * Copyright (c) 2021 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 *
+# * *
+# ***************************************************************************
+#
+# Script for manipulating toolbit shapes in a batch. This is handy for making
+# attributes consistent over multiple or all shape file.
+#
+# Most commands are straight forward, except for --set which can be used to
+# set the actual value of a property, or, in the case of enumerations it can
+# also be used to set the enum values. This might be particularly useful when
+# adding more materials.
+#
+# The following example moves all properties from the "Extra" group into the
+# "Attributes" group. Note that the Attributes group might or might not
+# already exist. If it does exist the specified group gets merged in.
+#
+# ./toolbit-attributes.py --move 'Extra:Attributes' src/Mod/Path/Tools/Shape/*.fcstd
+#
+# This example sets the Flutes value of all Shapes to 0:
+#
+# ./toolbit-attributes.py --set Flutes=0 src/Mod/Path/Tools/Shape/*.fcstd
+#
+# Finally, this example sets the enumerations of the Material attribute:
+#
+# ./toolbit-attributes.py --set 'Material=[HSS,Carbide,Tool Steel,Titanium]' src/Mod/Path/Tools/Shape/*.fcstd
+#
+# After running this tool it might be necessary to open the shape files
+# manually and make sure they are visible and the thumbprint image is
+# saved.
+#
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# !!! Final note: this is a dangerous tool and should not be shipped with FC. !!!!
+# !!! It's sole purpose is to make life of the developers easier and ensure !!!!
+# !!! consistent attributes across all toobit shapes. !!!!
+# !!! A single typo can ruin a lot of toolbit shapes - make sure to only use !!!!
+# !!! it if those shape files are under a version control system and you can !!!!
+# !!! back out the changes easily. !!!!
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+import argparse
+import os
+import sys
+
+parser = argparse.ArgumentParser()
+parser.add_argument('path', nargs='+', help='Shape file to process')
+parser.add_argument('--move', metavar=':', help='Move attributes from group 1 into group 2')
+parser.add_argument('--delete', metavar='prop', help='Delete the given attribute')
+parser.add_argument('--set', metavar='prop=value', help='Set property value')
+parser.add_argument('--print', action='store_true', help='If set attributes are printed as discovered')
+parser.add_argument('--print-all', action='store_true', help='If set Shape attributes are also printed')
+parser.add_argument('--print-groups', action='store_true', help='If set all custom property groups are printed')
+parser.add_argument('--save-changes', action='store_true', help='Unless specified the file is not saved')
+parser.add_argument('--freecad', help='Directory FreeCAD binaries (libFreeCAD.so) if not installed')
+args = parser.parse_args()
+
+if args.freecad:
+ sys.path.append(args.freecad)
+
+import FreeCAD
+import Path
+import PathScripts.PathPropertyBag as PathPropertyBag
+import PathScripts.PathUtil as PathUtil
+
+set_var=None
+set_val=None
+
+GroupMap = {}
+if args.move:
+ g = args.move.split(':')
+ if len(g) != 2:
+ print("ERROR: {} not a valid group mapping".format(args.move))
+ sys.exit(1)
+ GroupMap[g[0]] = g[1]
+
+if args.set:
+ s = args.set.split('=')
+ if len(s) != 2:
+ print("ERROR: {} not a valid group mapping".format(args.move))
+ sys.exit(1)
+ set_var = s[0]
+ set_val = s[1]
+
+for i, fname in enumerate(args.path):
+ #print(fname)
+ doc = FreeCAD.openDocument(fname, False)
+ print("{}:".format(doc.Name))
+ for o in doc.Objects:
+ if PathPropertyBag.IsPropertyBag(o):
+ if args.print_groups:
+ print(" {}: {}".format(o.Label, sorted(o.CustomPropertyGroups)))
+ else:
+ print(" {}:".format(o.Label))
+ for p in o.Proxy.getCustomProperties():
+ grp = o.getGroupOfProperty(p)
+ typ = o.getTypeIdOfProperty(p)
+ ttp = PathPropertyBag.getPropertyTypeName(typ)
+ val = PathUtil.getProperty(o, p)
+ dsc = o.getDocumentationOfProperty(p)
+ enm = ''
+ enum = []
+ if ttp == 'Enumeration':
+ enum = o.getEnumerationsOfProperty(p)
+ enm = "{}".format(','.join(enum))
+ if GroupMap.get(grp):
+ group = GroupMap.get(grp)
+ print("move: {}.{} -> {}".format(grp, p, group))
+ o.removeProperty(p)
+ o.Proxy.addCustomProperty(typ, p, group, dsc)
+ if enum:
+ print("enum {}.{}: {}".format(group, p, enum))
+ setattr(o, p, enum)
+ PathUtil.setProperty(o, p, val)
+ if p == set_var:
+ print("set {}.{} = {}".format(grp, p, set_val))
+ if ttp == 'Enumeration' and set_val[0] == '[':
+ enum = set_val[1:-1].split(',')
+ setattr(o, p, enum)
+ else:
+ PathUtil.setProperty(o, p, set_val)
+ if p == args.delete:
+ print("delete {}.{}".format(grp, p))
+ o.removeProperty(p)
+ if not args.print_all and grp == 'Shape':
+ continue
+ if args.print or args.print_all:
+ print(" {:10} {:20} {:20} {:10} {}".format(grp, p, ttp, str(val), enm))
+ o.Proxy.refreshCustomPropertyGroups()
+ if args.save_changes:
+ doc.recompute()
+ doc.save()
+ FreeCAD.closeDocument(doc.Name)
+
+print('-done-')