// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2024 Ondsel * * Copyright (c) 2025 Pieter Hijma * * * * This file is part of FreeCAD. * * * * FreeCAD is free software: you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 2.1 of the * * License, or (at your option) any later version. * * * * FreeCAD 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with FreeCAD. If not, see * * . * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Dialogs/DlgAddProperty.h" #include "Application.h" #include "Macro.h" #include "ui_DlgAddProperty.h" #include "ViewProviderVarSet.h" #include "propertyeditor/PropertyItem.h" FC_LOG_LEVEL_INIT("DlgAddProperty", true, true) using namespace Gui; using namespace Gui::Dialog; using namespace Gui::PropertyEditor; const std::string DlgAddProperty::GroupBase = "Base"; /* * This dialog has quite complex logic, so we will document it here. * * The design goals of this dialog are: * - support transactions (undo/redo), * - provide a value field as soon as possible (see #16189), * - keep the value if the name of the property is changed, * - support units (see #15557), * - support enumerations (see #15553), * - make OK available as soon as there is a valid property (see #17474), * - useful Python console commands (see #23760), * - support expressions (see #19716). * * Especially supporting expressions in the value field makes the logic * complex. Editors for value fields are created from PropertyItems. An * editor has two modes: One without the possibility to add an expression and * one with the possibility to add an expression. This depends on whether the * PropertyItem is bound. A PropertyItem can only be bound if a valid property * exists, which means the name of the property and the type should be known. * * As one of the goals of this dialog is to show an editor as soon as possible, * so also when there is no property name known yet, this means that the editor * won't have the expression button at first. * * To show the expression button as soon as possible, we create the property as * soon as a valid type and name are known. This allows us to bind the * PropertyItem which results in having the expression button. * * However, since we also want to support transactions, this means that we need * to open a transaction as well. This means that this dialog juggles the * following things: * * Given a valid property name and a property type we open a transaction and * create the property. As soon as the property name or type is changed, we * abort the transaction and start a new transaction with a recreated property * with the new name or type. * * If the property name or type is invalid, we clear the transaction and as * soon as the name or type become valid again, we start a new transaction. * * If the type is changed, we need to clear the current expression and value to * the default value. If only the name is changed, we keep the value as much * as possible with two exceptions: having a value based on an expression or * being a value for a property link. * * Expressions and links are bound to the property and changing the name of a * property prompts us to remove the old property and create a new one. This * to make sure that the transaction for adding a property does not keep a * history of old property name changes. So, because we want to have a new * transaction and expressions and links are bound to a property, the * expression or link becomes invalid when changing the property name. * * For expressions there are two choices: 1) we leave the outcome of the * expression in the value field (which is possible) but without it being based * on an expression or 2) we reset the value to the default value of the type. * * I chose the latter option because it is easy to miss that on a property name * change, the expression is invalidated, so the user may think the value is * the result of an expression, whereas in reality, the expression is lost. * * All in all, this leads to the following entities that need to be managed: * - transaction * - property item * - property * - editor * - value of the editor * * We have to react on a name change and on a type change. For each of these * changes, we need to take three cases into account: * - the name and type are valid * - only the type is valid * - neither the name nor the type is valid * * This has been encoded in the code below as onNameFieldChanged() and * onTypeChanged() and it works in two phases: clearing the transaction, * property item, and related, and building it up again depending on the * situation. */ DlgAddProperty::DlgAddProperty(QWidget* parent, App::PropertyContainer* container) : DlgAddProperty(parent, container, nullptr) {} DlgAddProperty::DlgAddProperty(QWidget* parent, ViewProviderVarSet* viewProvider) : DlgAddProperty( parent, viewProvider ? viewProvider->getObject() : nullptr, viewProvider ) {} DlgAddProperty::DlgAddProperty( QWidget* parent, App::PropertyContainer* container, ViewProviderVarSet* viewProvider ) : QDialog(parent) , container(container) , ui(new Ui_DlgAddProperty) , comboBoxGroup(this) , completerType(this) , editor(nullptr) , transactionID(0) { ui->setupUi(this); setupMacroRedirector(); initializeWidgets(viewProvider); } DlgAddProperty::~DlgAddProperty() = default; void DlgAddProperty::setupMacroRedirector() { setValueRedirector = std::make_unique( [this](MacroManager::LineType /*type*/, const char* line) { this->setValueCommand = line; } ); } int DlgAddProperty::findLabelRow(const char* labelName, QFormLayout* layout) { for (int row = 0; row < layout->rowCount(); ++row) { QLayoutItem* labelItem = layout->itemAt(row, QFormLayout::LabelRole); if (labelItem == nullptr) { continue; } if (auto label = qobject_cast(labelItem->widget())) { if (label->objectName() == QString::fromLatin1(labelName)) { return row; } } } return -1; } void DlgAddProperty::removeExistingWidget(QFormLayout* formLayout, int labelRow) { if (QLayoutItem* existingItem = formLayout->itemAt(labelRow, QFormLayout::FieldRole)) { if (QWidget* existingWidget = existingItem->widget()) { formLayout->removeWidget(existingWidget); existingWidget->deleteLater(); } } } void DlgAddProperty::setWidgetForLabel(const char* labelName, QWidget* widget, QLayout* layout) { auto formLayout = qobject_cast(layout); if (formLayout == nullptr) { FC_ERR("Form layout not found"); return; } int labelRow = findLabelRow(labelName, formLayout); if (labelRow < 0) { FC_ERR("Could not find row for '" << labelName << "'"); return; } removeExistingWidget(formLayout, labelRow); formLayout->setWidget(labelRow, QFormLayout::FieldRole, widget); } void DlgAddProperty::populateGroup(EditFinishedComboBox& comboBox, const App::PropertyContainer* container) { auto paramGroup = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/PropertyView" ); std::string lastGroup = paramGroup->GetASCII("NewPropertyGroup"); std::vector properties; container->getPropertyList(properties); std::unordered_set groupNames; for (const auto* prop : properties) { const char* groupName = container->getPropertyGroup(prop); groupNames.insert(groupName ? groupName : GroupBase); } std::vector groupNamesSorted(groupNames.begin(), groupNames.end()); std::ranges::sort(groupNamesSorted, [](const std::string& a, const std::string& b) { // prefer anything else other than Base, so move it to the back if (a == GroupBase) { return false; } if (b == GroupBase) { return true; } return a < b; }); for (const auto& groupName : groupNamesSorted) { comboBox.addItem(QString::fromStdString(groupName)); } if (!lastGroup.empty() && std::ranges::find(groupNames, lastGroup) != groupNames.end()) { comboBox.setEditText(QString::fromStdString(lastGroup)); } else { comboBox.setEditText(QString::fromStdString(groupNamesSorted[0])); } } void DlgAddProperty::initializeGroup() { comboBoxGroup.setObjectName(QStringLiteral("comboBoxGroup")); comboBoxGroup.setInsertPolicy(QComboBox::InsertAtTop); comboBoxGroup.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); setWidgetForLabel("labelGroup", &comboBoxGroup, layout()); populateGroup(comboBoxGroup, container); connComboBoxGroup = connect( &comboBoxGroup, &EditFinishedComboBox::editFinished, this, &DlgAddProperty::onGroupFinished ); } DlgAddProperty::SupportedTypes DlgAddProperty::getSupportedTypes() { std::vector commonTypes = { App::PropertyLength::getClassTypeId(), App::PropertyAngle::getClassTypeId(), App::PropertyFloat::getClassTypeId(), App::PropertyInteger::getClassTypeId(), App::PropertyBool::getClassTypeId(), App::PropertyString::getClassTypeId(), App::PropertyEnumeration::getClassTypeId(), }; std::vector otherTypes; std::vector allTypes; Base::Type::getAllDerivedFrom(Base::Type::fromName("App::Property"), allTypes); const auto isCommonType = [&commonTypes](const Base::Type& type) { return std::ranges::find(commonTypes, type) != commonTypes.end(); }; std::ranges::copy_if(allTypes, std::back_inserter(otherTypes), [&](const Base::Type& type) { return type.canInstantiate() && !isExcluded(type) && !isCommonType(type); }); std::ranges::sort(otherTypes, [](Base::Type a, Base::Type b) { return strcmp(a.getName(), b.getName()) < 0; }); return {.commonTypes = std::move(commonTypes), .otherTypes = std::move(otherTypes)}; } void DlgAddProperty::initializeTypes() { auto* model = new TypeItemModel(this); ui->comboBoxType->setModel(model); auto paramGroup = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/PropertyView" ); auto lastType = Base::Type::fromName( paramGroup->GetASCII("NewPropertyType", "App::PropertyLength").c_str() ); if (lastType.isBad()) { lastType = App::PropertyLength::getClassTypeId(); } const auto [commonTypes, otherTypes] = getSupportedTypes(); const auto addTypes = [this, &lastType](const std::vector& types) { for (const auto& type : types) { ui->comboBoxType->addItem(QString::fromLatin1(type.getName())); if (type == lastType) { ui->comboBoxType->setCurrentIndex(ui->comboBoxType->count() - 1); } } }; const auto addSeparator = [this]() { ui->comboBoxType->addItem(QStringLiteral("──────────────────────")); const int idx = ui->comboBoxType->count() - 1; ui->comboBoxType->setItemData(idx, true, TypeItemModel::SeparatorRole); }; addTypes(commonTypes); addSeparator(); addTypes(otherTypes); completerType.setModel(ui->comboBoxType->model()); completerType.setCaseSensitivity(Qt::CaseInsensitive); completerType.setFilterMode(Qt::MatchContains); ui->comboBoxType->setCompleter(&completerType); ui->comboBoxType->setInsertPolicy(QComboBox::NoInsert); connComboBoxType = connect( ui->comboBoxType, &QComboBox::currentTextChanged, this, &DlgAddProperty::onTypeChanged ); } void DlgAddProperty::removeSelectionEditor() { // If the editor has a lineedit, then Qt selects the string inside it when // the editor is created. This interferes with the editor getting focus. // For example, units will then be selected as well, whereas this is not // the behavior we want. We therefore deselect the text in the lineedit. if (auto lineEdit = editor->findChild()) { lineEdit->deselect(); } } void DlgAddProperty::addEnumEditor(PropertyItem* propertyItem) { auto* values = static_cast(PropertyStringListItem::create()); values->setParent(propertyItem); values->setPropertyName(QLatin1String(QT_TRANSLATE_NOOP("App::Property", "Enum"))); if (propertyItem->childCount() > 0) { auto* child = propertyItem->takeChild(0); delete child; } propertyItem->appendChild(values); editor.reset( values->createEditor(this, [this]() { this->valueChangedEnum(); }, FrameOption::WithFrame) ); } void DlgAddProperty::addNormalEditor(PropertyItem* propertyItem) { editor.reset( propertyItem->createEditor(this, [this]() { this->valueChanged(); }, FrameOption::WithFrame) ); } void DlgAddProperty::addEditor(PropertyItem* propertyItem) { if (!isTypeWithEditor(propertyItem)) { return; } if (isEnumPropertyItem()) { addEnumEditor(propertyItem); } else { addNormalEditor(propertyItem); } if (editor == nullptr) { return; } // Make sure that the editor has the same height as the // other widgets in the dialog. editor->setMinimumHeight(comboBoxGroup.height()); QSignalBlocker blockSignals(editor.get()); // To set the data in the editor, we need to set the data in the // propertyItem. The propertyItem needs to have a property set to make // sure that we get a correct value and the unit. setEditorData(propertyItem->data(PropertyItem::ValueColumn, Qt::EditRole)); editor->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); editor->setObjectName(QStringLiteral("editor")); setWidgetForLabel("labelValue", editor.get(), layout()); QWidget::setTabOrder(ui->lineEditName, editor.get()); QWidget::setTabOrder(editor.get(), ui->lineEditToolTip); removeSelectionEditor(); } bool DlgAddProperty::isExcluded(const Base::Type& type) const { // These properties are excluded because you cannot give them a value in // the property view. static const std::initializer_list excludedTypes = { App::PropertyBoolList::getClassTypeId(), App::PropertyColorList::getClassTypeId(), App::PropertyExpressionEngine::getClassTypeId(), App::PropertyIntegerSet::getClassTypeId(), App::PropertyMap::getClassTypeId(), App::PropertyMaterial::getClassTypeId(), App::PropertyPlacementList::getClassTypeId(), App::PropertyPythonObject::getClassTypeId(), App::PropertyUUID::getClassTypeId() }; std::string_view name(type.getName()); return !name.starts_with("App::Property") || std::ranges::find(excludedTypes, type) != excludedTypes.end(); } bool DlgAddProperty::isTypeWithEditor(PropertyItem* propertyItem) const { if (propertyItem == nullptr) { return false; } App::Property* prop = propertyItem->getFirstProperty(); if (prop == nullptr) { return false; } const Base::Type type = prop->getTypeId(); return isTypeWithEditor(type); } bool DlgAddProperty::isTypeWithEditor(const Base::Type& type) const { static const std::initializer_list subTypesWithEditor = { // These types and their subtypes have editors. App::PropertyBool::getClassTypeId(), App::PropertyColor::getClassTypeId(), App::PropertyFileIncluded::getClassTypeId(), App::PropertyFloat::getClassTypeId(), App::PropertyInteger::getClassTypeId() }; static const std::initializer_list typesWithEditor = { // These types have editors but not necessarily their subtypes. // Although sublink properties have editors, they need the 3D view to // select an object. Because the dialog is modal, it is not possible // to make use of the 3D view, hence we do not provide an editor for // sublinks and their lists. It is possible to create a property of // this type though and the property can be set in the property view // later which does give access to the 3D view. App::PropertyEnumeration::getClassTypeId(), App::PropertyFile::getClassTypeId(), App::PropertyFloatList::getClassTypeId(), App::PropertyFont::getClassTypeId(), App::PropertyIntegerList::getClassTypeId(), App::PropertyLink::getClassTypeId(), App::PropertyLinkList::getClassTypeId(), App::PropertyXLink::getClassTypeId(), App::PropertyXLinkList::getClassTypeId(), App::PropertyMaterialList::getClassTypeId(), App::PropertyPath::getClassTypeId(), App::PropertyString::getClassTypeId(), App::PropertyStringList::getClassTypeId(), App::PropertyVectorList::getClassTypeId() }; const auto isDerivedFromType = [&type](const Base::Type& t) { return type.isDerivedFrom(t); }; return std::ranges::find(typesWithEditor, type) != typesWithEditor.end() || std::ranges::any_of(subTypesWithEditor, isDerivedFromType); } bool DlgAddProperty::isTypeWithEditor(const std::string& type) const { Base::Type propType = Base::Type::getTypeIfDerivedFrom(type.c_str(), App::Property::getClassTypeId(), true); return isTypeWithEditor(propType); } static PropertyItem* createPropertyItem(App::Property* prop) { const char* editor = prop->getEditorName(); if (Base::Tools::isNullOrEmpty(editor)) { return nullptr; } return PropertyItemFactory::instance().createPropertyItem(editor); } void DlgAddProperty::createSupportDataForType(const Base::Type& type) { // Temporarily create a property for two reasons: // - to acquire the editor name from the instance, and // - to acquire an initial value from the instance possibly with the correct unit. void* propInstance = type.createInstance(); if (!propInstance) { FC_THROWM(Base::RuntimeError, "Failed to create a property of type " << type.getName()); } // When prop goes out of scope, it can be deleted because we obtained the // propertyItem (if applicable) and we initialized the editor with the data // from the property. std::unique_ptr prop( static_cast(propInstance), [](App::Property* p) { delete p; } ); prop->setContainer(container); propertyItem.reset(createPropertyItem(prop.get())); if (propertyItem && isTypeWithEditor(type)) { propertyItem->setPropertyData({prop.get()}); addEditor(propertyItem.get()); propertyItem->removeProperty(prop.get()); } } void DlgAddProperty::initializeValue() { std::string type = ui->comboBoxType->currentText().toStdString(); Base::Type propType = Base::Type::getTypeIfDerivedFrom(type.c_str(), App::Property::getClassTypeId(), true); if (propType.isBad()) { return; } createSupportDataForType(propType); if (!isTypeWithEditor(propType)) { // remove the editor from a previous add removeEditor(); } } void DlgAddProperty::setTitle() { setWindowTitle(tr("Add Property")); } void DlgAddProperty::setAddEnabled(bool enabled) { QPushButton* addButton = ui->buttonBox->button(QDialogButtonBox::Ok); QPushButton* closeButton = ui->buttonBox->button(QDialogButtonBox::Close); closeButton->setDefault(!enabled); addButton->setDefault(enabled); addButton->setEnabled(enabled); } void DlgAddProperty::initializeWidgets(ViewProviderVarSet* viewProvider) { initializeGroup(); initializeTypes(); initializeValue(); if (viewProvider) { connect(this, &QDialog::finished, this, [viewProvider](int result) { viewProvider->onFinished(result); }); } connLineEditNameTextChanged = connect(ui->lineEditName, &QLineEdit::textChanged, this, &DlgAddProperty::onNameChanged); setTitle(); QPushButton* addButton = ui->buttonBox->button(QDialogButtonBox::Ok); addButton->setText(tr("Add")); setAddEnabled(false); comboBoxGroup.setFocus(); QWidget::setTabOrder(&comboBoxGroup, ui->comboBoxType); QWidget::setTabOrder(ui->comboBoxType, ui->lineEditName); QWidget::setTabOrder(ui->lineEditName, editor.get()); QWidget::setTabOrder(editor.get(), ui->lineEditToolTip); adjustSize(); } bool DlgAddProperty::propertyExists(const std::string& name) { App::Property* prop = container->getPropertyByName(name.c_str()); return prop && prop->getContainer() == container && !(propertyItem && propertyItem->getFirstProperty() == prop); } bool DlgAddProperty::isNameValid() { std::string name = ui->lineEditName->text().toStdString(); return !name.empty() && name == Base::Tools::getIdentifier(name) && !App::ExpressionParser::isTokenAConstant(name) && !App::ExpressionParser::isTokenAUnit(name) && !propertyExists(name); } bool DlgAddProperty::isGroupValid() { std::string group = comboBoxGroup.currentText().toStdString(); return !group.empty() && group == Base::Tools::getIdentifier(group); } bool DlgAddProperty::isTypeValid() { std::string type = ui->comboBoxType->currentText().toStdString(); return Base::Type::fromName(type.c_str()).isDerivedFrom(App::Property::getClassTypeId()) && type != "App::Property"; } bool DlgAddProperty::isDocument() const { return container->isDerivedFrom(); } bool DlgAddProperty::isDocumentObject() const { return container->isDerivedFrom(); } bool DlgAddProperty::areFieldsValid() { return isNameValid() && isGroupValid() && isTypeValid(); } void DlgAddProperty::showStatusMessage() { QString error; QString text = ui->lineEditName->text(); std::string name = text.toStdString(); if (!isGroupValid()) { error = tr("Invalid group name"); } else if (!isTypeValid()) { error = tr("Invalid type name"); } else if (name.empty()) { error.clear(); } else if (name != Base::Tools::getIdentifier(name)) { error = tr("Invalid property name '%1'").arg(text); } else if (propertyExists(name)) { error = tr("Property '%1' already exists").arg(text); } else if (App::ExpressionParser::isTokenAConstant(name)) { error = tr("'%1' is a constant").arg(text); } else if (App::ExpressionParser::isTokenAUnit(name)) { error = tr("'%1' is a unit").arg(text); } ui->labelError->setText(error); } void DlgAddProperty::removeEditor() { if (editor == nullptr) { return; } // Create a placeholder widget to keep the layout intact. auto* placeholder = new QWidget(this); placeholder->setObjectName(QStringLiteral("placeholder")); placeholder->setMinimumHeight(comboBoxGroup.height()); setWidgetForLabel("labelValue", placeholder, layout()); QWidget::setTabOrder(ui->lineEditName, ui->lineEditToolTip); editor = nullptr; } bool DlgAddProperty::isEnumPropertyItem() const { return ui->comboBoxType->currentText() == QString::fromLatin1(App::PropertyEnumeration::getClassTypeId().getName()); } QVariant DlgAddProperty::getEditorData() const { if (isEnumPropertyItem()) { PropertyItem* child = propertyItem->child(0); if (child == nullptr) { return {}; } return child->editorData(editor.get()); } return propertyItem->editorData(editor.get()); } void DlgAddProperty::setEditorData(const QVariant& data) { if (isEnumPropertyItem()) { PropertyItem* child = propertyItem->child(0); if (child == nullptr) { return; } child->setEditorData(editor.get(), data); } else { propertyItem->setEditorData(editor.get(), data); } } void DlgAddProperty::setEditor(bool valueNeedsReset) { if (editor && !valueNeedsReset) { QVariant data = getEditorData(); addEditor(propertyItem.get()); if (editor == nullptr) { return; } setEditorData(data); removeSelectionEditor(); } else if (propertyItem) { addEditor(propertyItem.get()); } else { initializeValue(); } if (editor) { QVariant data = propertyItem->editorData(editor.get()); propertyItem->setData(data); } } void DlgAddProperty::setPropertyItem(App::Property* prop, bool supportsExpressions) { if (prop == nullptr) { return; } if (propertyItem == nullptr) { propertyItem.reset(createPropertyItem(prop)); } if (propertyItem == nullptr) { return; } propertyItem->setAutoApply(true); propertyItem->setPropertyData({prop}); if (supportsExpressions) { objectIdentifier = std::make_unique(*prop); propertyItem->bind(*objectIdentifier); } } void DlgAddProperty::buildForUnbound(bool valueNeedsReset) { setEditor(valueNeedsReset); } void DlgAddProperty::buildForBound(bool valueNeedsReset, bool supportsExpressions) { openTransaction(); App::Property* prop = createProperty(); setPropertyItem(prop, supportsExpressions); setEditor(valueNeedsReset); } bool DlgAddProperty::clearBoundProperty() { // Both a property link and an expression are bound to a property and as a // result need the value to be reset. bool isPropertyLinkItem = qobject_cast(propertyItem.get()) != nullptr; bool valueNeedsReset = isPropertyLinkItem || propertyItem->hasExpression(); if (App::Property* prop = propertyItem->getFirstProperty()) { propertyItem->unbind(); propertyItem->removeProperty(prop); container->removeDynamicProperty(prop->getName()); closeTransaction(TransactionOption::Abort); } return valueNeedsReset; } bool DlgAddProperty::clear(FieldChange fieldChange) { if (propertyItem == nullptr) { return true; } bool valueNeedsReset = clearBoundProperty(); if (fieldChange == FieldChange::Type) { valueNeedsReset = true; removeEditor(); propertyItem = nullptr; } return valueNeedsReset; } void DlgAddProperty::onNameChanged([[maybe_unused]] const QString& text) { bool valueNeedsReset = clear(FieldChange::Name); if (isNameValid() && isTypeValid()) { buildForBound(valueNeedsReset, isDocumentObject()); } else if (isTypeValid()) { buildForUnbound(valueNeedsReset); } else { removeEditor(); propertyItem = nullptr; } setAddEnabled(areFieldsValid()); showStatusMessage(); } void DlgAddProperty::onGroupFinished() { if (isGroupValid() && propertyItem) { std::string group = comboBoxGroup.currentText().toStdString(); std::string doc = ui->lineEditToolTip->text().toStdString(); if (App::Property* prop = propertyItem->getFirstProperty(); prop && prop->getGroup() != group) { container->changeDynamicProperty(prop, group.c_str(), doc.c_str()); } } setAddEnabled(areFieldsValid()); showStatusMessage(); } void DlgAddProperty::onTypeChanged([[maybe_unused]] const QString& text) { bool valueNeedsReset = clear(FieldChange::Type); if (isNameValid() && isTypeValid()) { buildForBound(valueNeedsReset, isDocumentObject()); } else if (isTypeValid()) { buildForUnbound(valueNeedsReset); } // nothing if both name and type are invalid setAddEnabled(areFieldsValid()); showStatusMessage(); } void DlgAddProperty::changeEvent(QEvent* e) { if (e->type() == QEvent::LanguageChange) { ui->retranslateUi(this); setTitle(); } QDialog::changeEvent(e); } void DlgAddProperty::valueChangedEnum() { auto* propEnum = static_cast(propertyItem->getFirstProperty()); if (propEnum == nullptr || propertyItem->childCount() == 0) { return; } auto* values = static_cast(propertyItem->child(0)); QVariant data = values->editorData(editor.get()); QStringList enumValues = data.toStringList(); // convert to std::vector std::vector enumValuesVec; std::ranges::transform(enumValues, std::back_inserter(enumValuesVec), [](const QString& value) { return value.toStdString(); }); propEnum->setEnums(enumValuesVec); } void DlgAddProperty::valueChanged() { QVariant data = propertyItem->editorData(editor.get()); propertyItem->setData(data); } /* We use these functions rather than the functions provided by App::Document * because this dialog may be opened when another transaction is in progress. * An example is opening a sketch. If this dialog uses the functions provided * by App::Document, a reject of the dialog would close that transaction. By * checking whether the transaction ID is "our" transaction ID, we prevent this * behavior. */ void DlgAddProperty::openTransaction() { transactionID = App::GetApplication().setActiveTransaction("Add property"); } void DlgAddProperty::critical(const QString& title, const QString& text) { static bool criticalDialogShown = false; if (!criticalDialogShown) { Base::StateLocker locker(criticalDialogShown); QMessageBox::critical(this, title, text); } } void DlgAddProperty::recordMacroAdd( const App::PropertyContainer* container, const std::string& type, const std::string& name, const std::string& group, const std::string& doc ) const { std::ostringstream command; command << "App.getDocument('"; const App::Document* document = freecad_cast(container); const App::DocumentObject* object = freecad_cast(container); if (document) { command << document->getName() << "')"; } else if (object) { command << object->getDocument()->getName() << "')." << object->getNameInDocument(); } else { FC_ERR("Cannot record macro for container of type " << container->getTypeId().getName()); return; } command << ".addProperty('" << type << "', '" << name << "', '" << group << "', '" << doc + "')"; Application::Instance->macroManager()->addLine(Gui::MacroManager::App, command.str().c_str()); } App::Property* DlgAddProperty::createProperty() { std::string name = ui->lineEditName->text().toStdString(); std::string group = comboBoxGroup.currentText().toStdString(); std::string type = ui->comboBoxType->currentText().toStdString(); std::string doc = ui->lineEditToolTip->text().toStdString(); auto recordAddCommand = [this](MacroManager::LineType, const char* line) { this->addCommand = line; }; try { App::Property* prop = container->addDynamicProperty(type.c_str(), name.c_str(), group.c_str(), doc.c_str()); MacroManager::MacroRedirector redirector(recordAddCommand); recordMacroAdd(container, type, name, group, doc); return prop; } catch (Base::Exception& e) { e.reportException(); critical( QObject::tr("Add property"), QObject::tr("Failed to add property to '%1': %2") .arg(QString::fromLatin1(container->getFullName().c_str()), QString::fromUtf8(e.what())) ); return nullptr; } } void DlgAddProperty::closeTransaction(TransactionOption option) { if (transactionID == 0) { return; } App::GetApplication().closeActiveTransaction(static_cast(option), transactionID); transactionID = 0; } void DlgAddProperty::clearFields() { { QSignalBlocker blocker(ui->lineEditName); ui->lineEditName->clear(); } ui->lineEditToolTip->clear(); initializeValue(); setAddEnabled(false); } void DlgAddProperty::addDocumentation() { /* Since there is no check on documentation (we accept any string), there * is no signal handler for the documentation field. This method updates * the property that is being added with the text inserted as * documentation/tooltip. */ std::string group = comboBoxGroup.currentText().toStdString(); std::string doc = ui->lineEditToolTip->text().toStdString(); if (propertyItem == nullptr) { // If there is no property item, we cannot add documentation. return; } App::Property* prop = propertyItem->getFirstProperty(); if (prop == nullptr) { return; } container->changeDynamicProperty(prop, group.c_str(), doc.c_str()); } void DlgAddProperty::accept() { addDocumentation(); auto* object = freecad_cast(container); if (object) { object->ExpressionEngine.execute(); } closeTransaction(TransactionOption::Commit); setValueRedirector = nullptr; Application::Instance->macroManager()->addLine(MacroManager::LineType::App, addCommand.c_str()); Application::Instance->macroManager()->addLine(MacroManager::LineType::App, setValueCommand.c_str()); setupMacroRedirector(); std::string group = comboBoxGroup.currentText().toStdString(); std::string type = ui->comboBoxType->currentText().toStdString(); auto paramGroup = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/PropertyView" ); paramGroup->SetASCII("NewPropertyType", type.c_str()); paramGroup->SetASCII("NewPropertyGroup", group.c_str()); clearFields(); ui->lineEditName->setFocus(); // Note that we don't call QDialog::accept() here to keep the dialog // open for adding more properties. } void DlgAddProperty::reject() { if (propertyItem) { if (App::Property* prop = propertyItem->getFirstProperty()) { App::PropertyContainer* container = prop->getContainer(); container->removeDynamicProperty(prop->getName()); closeTransaction(TransactionOption::Abort); } } disconnect(connComboBoxGroup); disconnect(connComboBoxType); disconnect(connLineEditNameTextChanged); QDialog::reject(); } #include "moc_DlgAddProperty.cpp"