# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2025 Samuel Abels * # * * # * 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 * # * * # *************************************************************************** """ Widgets for editing specific types of DocumentObject properties. Includes a factory method to create the appropriate widget based on type. """ import FreeCAD import FreeCADGui from PySide import QtGui, QtCore from typing import Optional class BasePropertyEditorWidget(QtGui.QWidget): """ Base class for property editor widgets. Includes a factory method to create specific subclasses based on property type. """ # Signal emitted when the underlying property value might have changed propertyChanged = QtCore.Signal() def __init__(self, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None): super().__init__(parent) self._obj = obj self._prop_name = prop_name self._layout = QtGui.QHBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) self._editor_widget: QtGui.QWidget = None # The actual input widget (SpinBox, ComboBox) self._editor_mode: int = 0 # Default to editable self._is_read_only: bool = False self._update_editor_mode() self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) def attachTo(self, obj: FreeCAD.DocumentObject, prop_name: Optional[str] = None): """Attach the editor to a (potentially different) object/property.""" self._obj = obj self._prop_name = prop_name if prop_name else self._prop_name self._update_editor_mode() self.updateWidget() # Ensure widget reflects new state def _update_editor_mode(self): """Fetch and store the current editor mode for the property.""" if self._obj and self._prop_name: self._editor_mode = self._obj.getEditorMode(self._prop_name) self._is_read_only = self._editor_mode == 2 return self._editor_mode = 0 self._is_read_only = False def updateWidget(self): """Update the editor widget's display from the object property.""" # Implementation specific to subclasses raise NotImplementedError def updateProperty(self): """Update the object property from the editor widget's value.""" # Implementation specific to subclasses # Should emit propertyChanged signal if value actually changes raise NotImplementedError @classmethod def for_property( cls, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None ) -> "BasePropertyEditorWidget": """ Factory method to create the appropriate editor widget subclass. """ if not obj or not hasattr(obj, "getPropertyByName"): return LabelPropertyEditorWidget(obj, prop_name, parent) prop_value = obj.getPropertyByName(prop_name) prop_type = obj.getTypeIdOfProperty(prop_name) if isinstance(prop_value, FreeCAD.Units.Quantity): return QuantityPropertyEditorWidget(obj, prop_name, parent) elif isinstance(prop_value, bool): return BoolPropertyEditorWidget(obj, prop_name, parent) elif isinstance(prop_value, int): return IntPropertyEditorWidget(obj, prop_name, parent) elif prop_type == "App::PropertyEnumeration": return EnumPropertyEditorWidget(obj, prop_name, parent) else: # Default to a read-only label for other types return LabelPropertyEditorWidget(obj, prop_name, parent) class QuantityPropertyEditorWidget(BasePropertyEditorWidget): """Editor widget for Quantity properties.""" def __init__(self, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None): super().__init__(obj, prop_name, parent) ui = FreeCADGui.UiLoader() self._editor_widget: FreeCADGui.QuantitySpinBox = ui.createWidget("Gui::QuantitySpinBox") self._layout.addWidget(self._editor_widget) self.updateWidget() # Set initial value # Connect signal after setting initial value to avoid premature update self._editor_widget.editingFinished.connect(self.updateProperty) def updateWidget(self): value: FreeCAD.Units.Quantity = self._obj.getPropertyByName(self._prop_name) # Block signals temporarily to prevent feedback loops self._editor_widget.blockSignals(True) self._editor_widget.setProperty("value", value) self._editor_widget.blockSignals(False) self._editor_widget.setEnabled(not self._is_read_only) def updateProperty(self): current_value = self._obj.getPropertyByName(self._prop_name) new_value_str: str = self._editor_widget.property("value").UserString new_value = FreeCAD.Units.Quantity(new_value_str) if new_value_str != current_value: self._obj.setPropertyByName(self._prop_name, new_value) self.propertyChanged.emit() class BoolPropertyEditorWidget(BasePropertyEditorWidget): """Editor widget for Boolean properties.""" def __init__(self, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None): super().__init__(obj, prop_name, parent) self._editor_widget: QtGui.QComboBox = QtGui.QComboBox() self._editor_widget.addItems(["False", "True"]) self._layout.addWidget(self._editor_widget) self.updateWidget() self._editor_widget.currentIndexChanged.connect(self._on_index_changed) def updateWidget(self): value: bool = self._obj.getPropertyByName(self._prop_name) self._editor_widget.blockSignals(True) self._editor_widget.setCurrentIndex(1 if value else 0) self._editor_widget.blockSignals(False) self._editor_widget.setEnabled(not self._is_read_only) def _on_index_changed(self, index: int): """Slot connected to currentIndexChanged signal.""" if self._is_read_only: return current_value: bool = self._obj.getPropertyByName(self._prop_name) new_value: bool = bool(index) if new_value != current_value: self._obj.setPropertyByName(self._prop_name, new_value) self.propertyChanged.emit() def updateProperty(self): """Update property based on current widget state (for consistency).""" self._on_index_changed(self._editor_widget.currentIndex()) class IntPropertyEditorWidget(BasePropertyEditorWidget): """Editor widget for Integer properties.""" def __init__(self, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None): super().__init__(obj, prop_name, parent) self._editor_widget: QtGui.QSpinBox = QtGui.QSpinBox() self._editor_widget.setMinimum(-2147483648) self._editor_widget.setMaximum(2147483647) self._layout.addWidget(self._editor_widget) self.updateWidget() self._editor_widget.editingFinished.connect(self.updateProperty) def updateWidget(self): value = self._obj.getPropertyByName(self._prop_name) self._editor_widget.blockSignals(True) self._editor_widget.setValue(value or 0) self._editor_widget.blockSignals(False) self._editor_widget.setEnabled(not self._is_read_only) def updateProperty(self): current_value: int = self._obj.getPropertyByName(self._prop_name) new_value: int = self._editor_widget.value() if new_value != current_value: self._obj.setPropertyByName(self._prop_name, new_value) self.propertyChanged.emit() class EnumPropertyEditorWidget(BasePropertyEditorWidget): """Editor widget for Enumeration properties.""" def __init__(self, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None): super().__init__(obj, prop_name, parent) self._editor_widget: QtGui.QComboBox = QtGui.QComboBox() self._layout.addWidget(self._editor_widget) self._populate_enum() self.updateWidget() self._editor_widget.currentIndexChanged.connect(self._on_index_changed) def _populate_enum(self): self._editor_widget.clear() enums: list[str] = self._obj.getEnumerationsOfProperty(self._prop_name) self._editor_widget.addItems(enums) def attachTo(self, obj: FreeCAD.DocumentObject, prop_name: Optional[str] = None): """Override attachTo to repopulate enums if object changes.""" super().attachTo(obj, prop_name) self._populate_enum() # Repopulate in case enums are different def updateWidget(self): value: str = self._obj.getPropertyByName(self._prop_name) self._editor_widget.blockSignals(True) index: int = self._editor_widget.findText(value) self._editor_widget.setCurrentIndex(index if index >= 0 else 0) self._editor_widget.blockSignals(False) self._editor_widget.setEnabled(not self._is_read_only) def _on_index_changed(self, index: int): """Slot connected to currentIndexChanged signal.""" if self._is_read_only: return current_value: str = self._obj.getPropertyByName(self._prop_name) new_value: str = self._editor_widget.itemText(index) if new_value != current_value: self._obj.setPropertyByName(self._prop_name, new_value) self.propertyChanged.emit() def updateProperty(self): """Update property based on current widget state (for consistency).""" self._on_index_changed(self._editor_widget.currentIndex()) class LabelPropertyEditorWidget(BasePropertyEditorWidget): """Read-only label for unsupported or invalid property types.""" def __init__(self, obj: FreeCAD.DocumentObject, prop_name: str, parent: QtGui.QWidget = None): super().__init__(obj, prop_name, parent) self._editor_widget: QtGui.QLabel = QtGui.QLabel("N/A") self._editor_widget.setTextInteractionFlags( QtGui.Qt.TextSelectableByMouse | QtGui.Qt.TextSelectableByKeyboard ) self._layout.addWidget(self._editor_widget) self.updateWidget() def updateWidget(self): text = "N/A" text: str = str(self._obj.getPropertyByName(self._prop_name)) self._editor_widget.setText(text) def updateProperty(self): # Read-only, no action needed pass