| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| | import FreeCADGui |
| | import Path |
| | import Path.Base.Util as PathUtil |
| | from PySide import QtGui, QtCore |
| |
|
| | from PySide import QtCore, QtGui |
| |
|
| | __title__ = "CAM UI helper and utility functions" |
| | __author__ = "sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "A collection of helper and utility functions for the CAM GUI." |
| |
|
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | def populateCombobox(form, enumTups, comboBoxesPropertyMap): |
| | """populateCombobox(form, enumTups, comboBoxesPropertyMap) ... populate comboboxes with translated enumerations |
| | ** comboBoxesPropertyMap will be unnecessary if UI files use strict combobox naming protocol. |
| | Args: |
| | form = UI form |
| | enumTups = list of (translated_text, data_string) tuples |
| | comboBoxesPropertyMap = list of (translated_text, data_string) tuples |
| | """ |
| | Path.Log.track(enumTups) |
| |
|
| | |
| | for cb, prop in comboBoxesPropertyMap: |
| | box = getattr(form, cb) |
| | box.clear() |
| | for text, data in enumTups[prop]: |
| | box.addItem(text, data) |
| |
|
| |
|
| | def updateInputField(obj, prop, widget, onBeforeChange=None): |
| | """updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget. |
| | The property's value is only assigned if the new value differs from the current value. |
| | This prevents onChanged notifications where the value didn't actually change. |
| | Gui::InputField and Gui::QuantitySpinBox widgets are supported - and the property can |
| | be of type Quantity or Float. |
| | 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). |
| | """ |
| | value = widget.property("rawValue") |
| | Path.Log.track("value: {}".format(value)) |
| | attr = PathUtil.getProperty(obj, prop) |
| | attrValue = attr.Value if hasattr(attr, "Value") else attr |
| |
|
| | isDiff = False |
| | if not Path.Geom.isRoughly(attrValue, value): |
| | isDiff = True |
| | else: |
| | if hasattr(obj, "ExpressionEngine"): |
| | exprSet = False |
| | for prp, expr in obj.ExpressionEngine: |
| | if prp == prop: |
| | exprSet = True |
| | Path.Log.debug('prop = "expression": {} = "{}"'.format(prp, expr)) |
| | value = FreeCAD.Units.Quantity(obj.evalExpression(expr)).Value |
| | if not Path.Geom.isRoughly(attrValue, value): |
| | isDiff = True |
| | break |
| | if exprSet: |
| | widget.setReadOnly(True) |
| | widget.setProperty("exprSet", "true") |
| | widget.style().unpolish(widget) |
| | widget.ensurePolished() |
| | else: |
| | widget.setReadOnly(False) |
| | widget.setProperty("exprSet", "false") |
| | widget.style().unpolish(widget) |
| | widget.ensurePolished() |
| | widget.update() |
| |
|
| | if isDiff: |
| | Path.Log.debug("updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value)) |
| | if onBeforeChange: |
| | onBeforeChange(obj) |
| | PathUtil.setProperty(obj, prop, value) |
| | return True |
| |
|
| | return False |
| |
|
| |
|
| | class QuantitySpinBox(QtCore.QObject): |
| | """Controller class to interface a Gui::QuantitySpinBox. |
| | The spin box gets bound to a given property and supports update in both directions. |
| | QuatitySpinBox(widget, obj, prop, onBeforeChange=None) |
| | widget ... expected to be reference to a Gui::QuantitySpinBox |
| | obj ... document object |
| | prop ... canonical name of the (sub-) property |
| | onBeforeChange ... an optional callback being executed before the value of the property is changed |
| | """ |
| |
|
| | def __init__(self, widget, obj, prop, onBeforeChange=None): |
| | super().__init__() |
| | Path.Log.track(widget) |
| | self.widget = widget |
| | self.onBeforeChange = onBeforeChange |
| | self.prop = None |
| | self.obj = obj |
| | self.lastWidgetText = self.widget.text() |
| | self.attachTo(obj, prop) |
| | self.widget.installEventFilter(self) |
| | |
| | self.widget.textChanged.connect(self.onWidgetValueChanged) |
| | |
| | try: |
| | self.widget.showFormulaDialog.connect(self.onFormulaDialogStateChanged) |
| | except AttributeError: |
| | |
| | pass |
| |
|
| | def onFormulaDialogStateChanged(self, isOpen): |
| | """ |
| | Slot called when the formula dialog is opened or closed. |
| | When the dialog closes (isOpen=False), refresh the widget. |
| | """ |
| | if not isOpen: |
| | |
| | self.updateWidget() |
| |
|
| | def eventFilter(self, obj, event): |
| | if event.type() == QtCore.QEvent.Type.FocusIn: |
| | self.updateWidget() |
| | return False |
| |
|
| | def onWidgetValueChanged(self): |
| | """ |
| | Slot method for determining if a change |
| | in widget value is a result of an expression edit, or a simple spinbox change. |
| | If the former, emit a manual `editingFinished` signal because the Expression editor |
| | window returned a value to the base widget, leaving it in read-only mode, |
| | and finishing the editing of the value. Otherwise, due nothing if the value |
| | has not changed, or there is no active expression for the property. |
| | If the user closes the Expression editor to cancel the edit, the value will not |
| | be changed, and this manual signal will not be emitted.""" |
| | if self._hasExpression() and self.widget.text() != self.lastWidgetText: |
| | self.widget.editingFinished.emit() |
| |
|
| | def attachTo(self, obj, prop=None): |
| | """use an existing editor for the given object and property""" |
| | Path.Log.track(self.prop, prop) |
| | self.obj = obj |
| | self.prop = prop |
| | if obj and prop: |
| | attr = PathUtil.getProperty(obj, prop) |
| | if attr is not None: |
| | if hasattr(attr, "Value"): |
| | self.widget.setProperty("unit", attr.getUserPreferred()[2]) |
| | self.widget.setProperty("binding", "%s.%s" % (obj.Name, prop)) |
| | self.valid = True |
| | else: |
| | Path.Log.warning("Cannot find property {} of {}".format(prop, obj.Label)) |
| | self.valid = False |
| | else: |
| | self.valid = False |
| |
|
| | def expression(self): |
| | """returns the expression if one is bound to the property""" |
| | Path.Log.track(self.prop, self.valid) |
| | if self.valid: |
| | return self.widget.property("expression") |
| | return "" |
| |
|
| | def setMinimum(self, quantity): |
| | """set the minimum""" |
| | Path.Log.track(self.prop, self.valid) |
| | if self.valid: |
| | value = quantity.Value if hasattr(quantity, "Value") else quantity |
| | self.widget.setProperty("setMinimum", value) |
| |
|
| | def updateWidget(self, 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.""" |
| | Path.Log.track(self.prop, self.valid, quantity) |
| |
|
| | if self.valid: |
| | expr = self._hasExpression() |
| | if quantity is None: |
| | if expr: |
| | quantity = FreeCAD.Units.Quantity(self.obj.evalExpression(expr)) |
| | else: |
| | quantity = PathUtil.getProperty(self.obj, self.prop) |
| | value = quantity.Value if hasattr(quantity, "Value") else quantity |
| | self.widget.setProperty("rawValue", value) |
| | self.lastWidgetText = self.widget.text() |
| | if expr: |
| | self.widget.setReadOnly(True) |
| | self.widget.setProperty("exprSet", "true") |
| | self.widget.style().unpolish(self.widget) |
| | self.widget.ensurePolished() |
| | else: |
| | self.widget.setReadOnly(False) |
| | self.widget.setProperty("exprSet", "false") |
| | self.widget.style().unpolish(self.widget) |
| | self.widget.ensurePolished() |
| |
|
| | def updateProperty(self): |
| | """updateProperty() ... update the bound property with the value from the spin box""" |
| | Path.Log.track(self.prop, self.valid) |
| | if self.valid: |
| | return updateInputField(self.obj, self.prop, self.widget, self.onBeforeChange) |
| | return None |
| |
|
| | def _hasExpression(self): |
| | for prop, exp in self.obj.ExpressionEngine: |
| | if prop == self.prop: |
| | return exp |
| | return None |
| |
|
| |
|
| | class PropertyComboBox(QtCore.QObject): |
| | """Base controller class for properties represented as QComboBox.""" |
| |
|
| | def __init__(self, widget, obj, prop, onBeforeChange=None): |
| | super().__init__() |
| | Path.Log.track(widget) |
| | self.widget = widget |
| | self.onBeforeChange = onBeforeChange |
| | self.prop = None |
| | self.obj = obj |
| | self.valid = False |
| | self.attachTo(obj, prop) |
| | self.widget.currentIndexChanged.connect(self.updateProperty) |
| |
|
| | def attachTo(self, obj, prop=None): |
| | """use an existing editor for the given object and property""" |
| | Path.Log.track(self.prop, prop) |
| | self.obj = obj |
| | self.prop = prop |
| | if obj and prop: |
| | attr = PathUtil.getProperty(obj, prop) |
| | if attr is not None: |
| | self.valid = True |
| | self._populateComboBox() |
| | self.updateWidget() |
| | else: |
| | Path.Log.warning("Cannot find property {} of {}".format(prop, obj.Label)) |
| | self.valid = False |
| | else: |
| | self.valid = False |
| |
|
| | def _populateComboBox(self): |
| | """To be implemented by subclasses""" |
| | raise NotImplementedError |
| |
|
| | def updateWidget(self, value=None): |
| | """update the display value of the combo box.""" |
| | Path.Log.track(self.prop, self.valid, value) |
| | if self.valid: |
| | if value is None: |
| | value = PathUtil.getProperty(self.obj, self.prop) |
| | index = ( |
| | self.widget.findData(value) |
| | if hasattr(self.widget, "findData") |
| | else self.widget.findText(str(value)) |
| | ) |
| | if index >= 0: |
| | self.widget.setCurrentIndex(index) |
| |
|
| | def updateProperty(self): |
| | """update the bound property with the value from the combo box""" |
| | Path.Log.track(self.prop, self.valid) |
| | if self.valid and self.prop: |
| | if self.onBeforeChange: |
| | self.onBeforeChange() |
| |
|
| | current_value = PathUtil.getProperty(self.obj, self.prop) |
| | new_value = ( |
| | self.widget.currentData() |
| | if hasattr(self.widget, "currentData") |
| | else self.widget.currentText() |
| | ) |
| |
|
| | if str(new_value) != str(current_value): |
| | setattr(self.obj, self.prop, new_value) |
| | return True |
| | return False |
| |
|
| |
|
| | class IntegerSpinBox(QtCore.QObject): |
| | """Controller class for integer properties represented as QSpinBox. |
| | IntegerSpinBox(widget, obj, prop, onBeforeChange=None) |
| | widget ... expected to be reference to a QSpinBox |
| | obj ... document object |
| | prop ... canonical name of the (sub-) property |
| | onBeforeChange ... optional callback before property change |
| | """ |
| |
|
| | def __init__(self, widget, obj, prop, onBeforeChange=None): |
| | super().__init__() |
| | self.widget = widget |
| | self.onBeforeChange = onBeforeChange |
| | self.prop = None |
| | self.obj = obj |
| | self.valid = False |
| |
|
| | |
| | self.widget.setMinimum(-2147483647) |
| | self.widget.setMaximum(2147483647) |
| |
|
| | self.attachTo(obj, prop) |
| | self.widget.valueChanged.connect(self.updateProperty) |
| |
|
| | def attachTo(self, obj, prop=None): |
| | """bind to the given object and property""" |
| | self.obj = obj |
| | self.prop = prop |
| | if obj and prop: |
| | try: |
| | prop_value = PathUtil.getProperty(obj, prop) |
| | if prop_value is not None: |
| | self.valid = True |
| | self.updateWidget() |
| | else: |
| | Path.Log.warning(f"Cannot get value for property {prop} of {obj.Label}") |
| | self.valid = False |
| | except Exception as e: |
| | Path.Log.error(f"Error attaching to property {prop}: {str(e)}") |
| | self.valid = False |
| | else: |
| | self.valid = False |
| |
|
| | def updateWidget(self, value=None): |
| | """update the spin box value""" |
| | if self.valid: |
| | try: |
| | if value is None: |
| | value = PathUtil.getProperty(self.obj, self.prop) |
| |
|
| | |
| | if hasattr(value, "Value"): |
| | value = int(value.Value) |
| |
|
| | self.widget.setValue(int(value)) |
| | except Exception as e: |
| | Path.Log.error(f"Error updating spin box: {str(e)}") |
| |
|
| | def updateProperty(self): |
| | """update the bound property with the spin box value""" |
| | if self.valid and self.prop: |
| | if self.onBeforeChange: |
| | self.onBeforeChange() |
| |
|
| | new_value = self.widget.value() |
| | current_value = PathUtil.getProperty(self.obj, self.prop) |
| |
|
| | |
| | if hasattr(current_value, "Value"): |
| | if new_value != current_value.Value: |
| | current_value.Value = new_value |
| | return True |
| | elif new_value != current_value: |
| | setattr(self.obj, self.prop, new_value) |
| | return True |
| | return False |
| |
|
| | def setRange(self, min_val, max_val): |
| | """set minimum and maximum values""" |
| | self.widget.setMinimum(min_val) |
| | self.widget.setMaximum(max_val) |
| |
|
| | def setSingleStep(self, step): |
| | """setSingleStep(step) ... set the step size""" |
| | self.widget.setSingleStep(step) |
| |
|
| |
|
| | class BooleanComboBox(PropertyComboBox): |
| | """Controller class for boolean properties represented as QComboBox.""" |
| |
|
| | def _populateComboBox(self): |
| | self.widget.clear() |
| | self.widget.addItem("True", True) |
| | self.widget.addItem("False", False) |
| |
|
| |
|
| | class EnumerationComboBox(PropertyComboBox): |
| | """Controller class for enumeration properties represented as QComboBox.""" |
| |
|
| | def _populateComboBox(self): |
| | self.widget.clear() |
| | enums = self.obj.getEnumerationsOfProperty(self.prop) |
| | for item in enums: |
| | self.widget.addItem(item, item) |
| |
|
| |
|
| | class PropertyLabel(QtCore.QObject): |
| | """Controller class for read-only property display as QLabel.""" |
| |
|
| | def __init__(self, widget, obj, prop, onBeforeChange=None): |
| | super().__init__() |
| | self.widget = widget |
| | self.obj = obj |
| | self.prop = prop |
| | self.valid = False |
| | self.attachTo(obj, prop) |
| |
|
| | def attachTo(self, obj, prop=None): |
| | """bind to the given object and property""" |
| | self.obj = obj |
| | self.prop = prop |
| | if obj and prop: |
| | attr = PathUtil.getProperty(obj, prop) |
| | if attr is not None: |
| | self.valid = True |
| | self.updateWidget() |
| | else: |
| | Path.Log.warning(f"Cannot find property {prop} of {obj.Label}") |
| | self.valid = False |
| | else: |
| | self.valid = False |
| |
|
| | def updateWidget(self, value=None): |
| | """update the label text""" |
| | if self.valid: |
| | if value is None: |
| | value = PathUtil.getProperty(self.obj, self.prop) |
| | self.widget.setText(str(value)) |
| |
|
| |
|
| | def getDocNode(): |
| | doc = FreeCADGui.ActiveDocument.Document.Name |
| | tws = FreeCADGui.getMainWindow().findChildren(QtGui.QTreeWidget) |
| |
|
| | for tw in tws: |
| | if tw.topLevelItemCount() != 1 or tw.topLevelItem(0).text(0) != "Application": |
| | continue |
| | toptree = tw.topLevelItem(0) |
| | for i in range(0, toptree.childCount()): |
| | docitem = toptree.child(i) |
| | if docitem.text(0) == doc: |
| | return docitem |
| | return None |
| |
|
| |
|
| | def disableItem(item): |
| | Dragflag = QtCore.Qt.ItemFlag.ItemIsDragEnabled |
| | Dropflag = QtCore.Qt.ItemFlag.ItemIsDropEnabled |
| | item.setFlags(item.flags() & ~Dragflag) |
| | item.setFlags(item.flags() & ~Dropflag) |
| | for idx in range(0, item.childCount()): |
| | disableItem(item.child(idx)) |
| |
|
| |
|
| | def findItem(docitem, objname): |
| | print(docitem.text(0)) |
| | for i in range(0, docitem.childCount()): |
| | if docitem.child(i).text(0) == objname: |
| | return docitem.child(i) |
| | res = findItem(docitem.child(i), objname) |
| | if res: |
| | return res |
| | return None |
| |
|