From 35cd20bb171858dfd5d3b6758a78cfe413c817eb Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Wed, 27 Aug 2025 15:37:31 +0200 Subject: [PATCH 1/9] Gui: Simplify title Add Property VarSet dialog --- src/Gui/Dialogs/DlgAddPropertyVarSet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp b/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp index db221c360e..78f67435fb 100644 --- a/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp +++ b/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp @@ -447,7 +447,7 @@ void DlgAddPropertyVarSet::initializeValue() void DlgAddPropertyVarSet::setTitle() { - setWindowTitle(tr("Add a Property to %1").arg(QString::fromStdString(varSet->getFullName()))); + setWindowTitle(tr("Add Property")); } void DlgAddPropertyVarSet::setOkEnabled(bool enabled) From 65a8597ad83fc21e8fabed4710bc05639534fe6e Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Thu, 28 Aug 2025 10:33:15 +0200 Subject: [PATCH 2/9] Gui: Generalize add property VarSet dialog This commit ensures that the adding properties does not work only for VarSets, but also for generic property containers. --- src/Gui/Dialogs/DlgAddPropertyVarSet.cpp | 54 ++++++++++++++++-------- src/Gui/Dialogs/DlgAddPropertyVarSet.h | 11 +++-- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp b/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp index 78f67435fb..d62b3c18dd 100644 --- a/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp +++ b/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp @@ -129,11 +129,26 @@ const std::string DlgAddPropertyVarSet::GroupBase = "Base"; * situation. */ +DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent, + App::PropertyContainer* container) + : DlgAddPropertyVarSet(parent, container, nullptr) +{ +} + DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent, ViewProviderVarSet* viewProvider) + : DlgAddPropertyVarSet(parent, + viewProvider ? viewProvider->getObject() : nullptr, + viewProvider) +{ +} + +DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent, + App::PropertyContainer* container, + ViewProviderVarSet* viewProvider) : QDialog(parent), - varSet(viewProvider->getObject()), + container(container), ui(new Ui_DlgAddPropertyVarSet), comboBoxGroup(this), completerType(this), @@ -194,14 +209,14 @@ void DlgAddPropertyVarSet::setWidgetForLabel(const char* labelName, QWidget* wid } void DlgAddPropertyVarSet::populateGroup(EditFinishedComboBox& comboBox, - const App::DocumentObject* varSet) + const App::PropertyContainer* container) { std::vector properties; - varSet->getPropertyList(properties); + container->getPropertyList(properties); std::unordered_set groupNames; for (const auto* prop : properties) { - const char* groupName = varSet->getPropertyGroup(prop); + const char* groupName = container->getPropertyGroup(prop); groupNames.insert(groupName ? groupName : GroupBase); } @@ -231,7 +246,7 @@ void DlgAddPropertyVarSet::initializeGroup() comboBoxGroup.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); setWidgetForLabel("labelGroup", &comboBoxGroup, layout()); - populateGroup(comboBoxGroup, varSet); + populateGroup(comboBoxGroup, container); connComboBoxGroup = connect(&comboBoxGroup, &EditFinishedComboBox::editFinished, this, &DlgAddPropertyVarSet::onGroupFinished); @@ -416,7 +431,7 @@ void DlgAddPropertyVarSet::createEditorForType(const Base::Type& type) std::unique_ptr prop( static_cast(propInstance), [](App::Property* p) { delete p; }); - prop->setContainer(varSet); + prop->setContainer(container); propertyItem.reset(createPropertyItem(prop.get())); @@ -462,8 +477,10 @@ void DlgAddPropertyVarSet::initializeWidgets(ViewProviderVarSet* viewProvider) initializeTypes(); initializeValue(); - connect(this, &QDialog::finished, - this, [viewProvider](int result) { viewProvider->onFinished(result); }); + if (viewProvider) { + connect(this, &QDialog::finished, + this, [viewProvider](int result) { viewProvider->onFinished(result); }); + } connLineEditNameTextChanged = connect(ui->lineEditName, &QLineEdit::textChanged, this, &DlgAddPropertyVarSet::onNameChanged); @@ -478,9 +495,9 @@ void DlgAddPropertyVarSet::initializeWidgets(ViewProviderVarSet* viewProvider) bool DlgAddPropertyVarSet::propertyExists(const std::string& name) { - App::Property* prop = varSet->getPropertyByName(name.c_str()); - return prop && prop->getContainer() == varSet && - !(propertyItem && propertyItem->getFirstProperty() == prop); + App::Property* prop = container->getPropertyByName(name.c_str()); + return prop && prop->getContainer() == container && + !(propertyItem && propertyItem->getFirstProperty() == prop); } bool DlgAddPropertyVarSet::isNameValid() @@ -658,7 +675,7 @@ bool DlgAddPropertyVarSet::clearBoundProperty() if (App::Property* prop = propertyItem->getFirstProperty()) { propertyItem->unbind(); propertyItem->removeProperty(prop); - varSet->removeDynamicProperty(prop->getName()); + container->removeDynamicProperty(prop->getName()); closeTransaction(TransactionOption::Abort); } return valueNeedsReset; @@ -705,7 +722,7 @@ void DlgAddPropertyVarSet::onGroupFinished() std::string doc = ui->lineEditToolTip->text().toStdString(); if (App::Property* prop = propertyItem->getFirstProperty(); prop && prop->getGroup() != group) { - varSet->changeDynamicProperty(prop, group.c_str(), doc.c_str()); + container->changeDynamicProperty(prop, group.c_str(), doc.c_str()); } } @@ -790,14 +807,14 @@ App::Property* DlgAddPropertyVarSet::createProperty() std::string doc = ui->lineEditToolTip->text().toStdString(); try { - return varSet->addDynamicProperty(type.c_str(), name.c_str(), + return container->addDynamicProperty(type.c_str(), name.c_str(), group.c_str(), doc.c_str()); } catch (Base::Exception& e) { e.reportException(); critical(QObject::tr("Add property"), QObject::tr("Failed to add property to '%1': %2").arg( - QString::fromLatin1(varSet->getFullName().c_str()), + QString::fromLatin1(container->getFullName().c_str()), QString::fromUtf8(e.what()))); return nullptr; } @@ -844,13 +861,16 @@ void DlgAddPropertyVarSet::addDocumentation() { return; } - varSet->changeDynamicProperty(prop, group.c_str(), doc.c_str()); + container->changeDynamicProperty(prop, group.c_str(), doc.c_str()); } void DlgAddPropertyVarSet::accept() { addDocumentation(); - varSet->ExpressionEngine.execute(); + auto* object = freecad_cast(container); + if (object) { + object->ExpressionEngine.execute(); + } closeTransaction(TransactionOption::Commit); std::string group = comboBoxGroup.currentText().toStdString(); std::string type = ui->comboBoxType->currentText().toStdString(); diff --git a/src/Gui/Dialogs/DlgAddPropertyVarSet.h b/src/Gui/Dialogs/DlgAddPropertyVarSet.h index 7978f5da77..60b1177af8 100644 --- a/src/Gui/Dialogs/DlgAddPropertyVarSet.h +++ b/src/Gui/Dialogs/DlgAddPropertyVarSet.h @@ -32,7 +32,7 @@ #include -#include +#include #include "propertyeditor/PropertyItem.h" @@ -75,6 +75,7 @@ public: public: DlgAddPropertyVarSet(QWidget* parent, ViewProviderVarSet* viewProvider); + DlgAddPropertyVarSet(QWidget* parent, App::PropertyContainer* container); DlgAddPropertyVarSet(const DlgAddPropertyVarSet&) = delete; DlgAddPropertyVarSet(DlgAddPropertyVarSet&&) = delete; @@ -86,7 +87,8 @@ public: void changeEvent(QEvent* e) override; void accept() override; void reject() override; - static void populateGroup(EditFinishedComboBox& comboBox, const App::DocumentObject* varSet); + static void populateGroup(EditFinishedComboBox& comboBox, + const App::PropertyContainer* container); static void setWidgetForLabel(const char* labelName, QWidget* widget, QLayout* layout); public Q_SLOTS: @@ -104,6 +106,9 @@ private: Type }; + DlgAddPropertyVarSet(QWidget* parent, App::PropertyContainer* container, + ViewProviderVarSet* viewProvider); + void initializeGroup(); std::vector getSupportedTypes(); @@ -156,7 +161,7 @@ private: static int findLabelRow(const char* labelName, QFormLayout* layout); private: - App::VarSet* varSet; + App::PropertyContainer* container; std::unique_ptr ui; EditFinishedComboBox comboBoxGroup; From 37e8a753b65b8b9e9558c793b7203152464d1344 Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Thu, 28 Aug 2025 12:54:28 +0200 Subject: [PATCH 3/9] Gui: Disallow adding props to multiple objects This commit is in preparation for switching the old Add Property dialog to the Add Property VarSet dialog. For now, this dialog does not handle adding properties for multiple objects. --- src/Gui/Tree.cpp | 33 ++++++++++++++++++++ src/Gui/Tree.h | 1 + src/Gui/propertyeditor/PropertyEditor.cpp | 37 +++++++++++++++-------- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index b6b68ed2b5..93321f06b6 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -1512,6 +1512,39 @@ void TreeWidget::setupResizableColumn(TreeWidget *tree) { } } +std::vector TreeWidget::getSelectedDocuments() { + std::vector ret; + TreeWidget* tree = instance(); + if (!tree || !tree->isSelectionAttached()) { + for (auto pTree : Instances) + if (pTree->isSelectionAttached()) { + tree = pTree; + break; + } + } + if (!tree) + return ret; + + if (tree->selectTimer->isActive()) + tree->onSelectTimer(); + else + tree->_updateStatus(false); + + const auto items = tree->selectedItems(); + for (auto ti : items) { + if (ti->type() != DocumentType) + continue; + auto item = static_cast(ti); + auto doc = item->document(); + if (!doc || !doc->getDocument()) { + FC_WARN("skip invalid document"); + continue; + } + ret.push_back(doc); + } + return ret; +} + std::vector TreeWidget::getSelection(App::Document* doc) { std::vector ret; diff --git a/src/Gui/Tree.h b/src/Gui/Tree.h index 72a048ef62..9555d1254f 100644 --- a/src/Gui/Tree.h +++ b/src/Gui/Tree.h @@ -92,6 +92,7 @@ public: * which Gui::Selection() cannot provide. */ static std::vector getSelection(App::Document *doc=nullptr); + static std::vector getSelectedDocuments(); static TreeWidget *instance(); diff --git a/src/Gui/propertyeditor/PropertyEditor.cpp b/src/Gui/propertyeditor/PropertyEditor.cpp index 6a6f62fad5..0ce318c66a 100644 --- a/src/Gui/propertyeditor/PropertyEditor.cpp +++ b/src/Gui/propertyeditor/PropertyEditor.cpp @@ -40,6 +40,8 @@ #include #include +#include "Document.h" +#include "Tree.h" #include "PropertyEditor.h" #include "Dialogs/DlgAddProperty.h" #include "MainWindow.h" @@ -775,6 +777,19 @@ enum MenuAction MA_Copy, }; +static App::PropertyContainer* getSelectedPropertyContainer() +{ + auto sels = Gui::Selection().getSelection("*"); + if (sels.size() == 1) { + return sels[0].pObject; + } + std::vector docs = Gui::TreeWidget::getSelectedDocuments(); + if (docs.size() == 1) { + return docs[0]->getDocument(); + } + return nullptr; +} + std::unordered_set PropertyEditor::acquireSelectedProperties() const { std::unordered_set props; @@ -831,7 +846,11 @@ void PropertyEditor::contextMenuEvent(QContextMenuEvent*) } // add property - menu.addAction(tr("Add Property"))->setData(QVariant(MA_AddProp)); + if (getSelectedPropertyContainer()) { + menu.addAction(tr("Add Property"))->setData(QVariant(MA_AddProp)); + } + + // rename property group if (!props.empty() && std::all_of(props.begin(), props.end(), [](auto prop) { return prop->testStatus(App::Property::PropDynamic) && !boost::starts_with(prop->getName(), prop->getGroup()); @@ -989,18 +1008,12 @@ void PropertyEditor::contextMenuEvent(QContextMenuEvent*) } break; case MA_AddProp: { + App::PropertyContainer* container = getSelectedPropertyContainer(); + if (container == nullptr) { + return; + } App::AutoTransaction committer("Add property"); - std::unordered_set containers; - auto sels = Gui::Selection().getSelection("*"); - if (sels.size() == 1) { - containers.insert(sels[0].pObject); - } - else { - for (auto prop : props) { - containers.insert(prop->getContainer()); - } - } - Gui::Dialog::DlgAddProperty dlg(Gui::getMainWindow(), std::move(containers)); + Gui::Dialog::DlgAddProperty dlg(Gui::getMainWindow(), container); dlg.exec(); return; } From 93a73d55e1c69c97ef550e169e5ef08e75c562da Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Thu, 28 Aug 2025 13:57:50 +0200 Subject: [PATCH 4/9] Gui: Move DlgAddPropertyVarSet -> DlgAddProperty --- src/Gui/CMakeLists.txt | 3 - src/Gui/Dialogs/DlgAddProperty.cpp | 1097 ++++++++++++++++++---- src/Gui/Dialogs/DlgAddProperty.h | 245 +++-- src/Gui/Dialogs/DlgAddProperty.ui | 98 +- src/Gui/Dialogs/DlgAddPropertyVarSet.cpp | 908 ------------------ src/Gui/Dialogs/DlgAddPropertyVarSet.h | 185 ---- src/Gui/Dialogs/DlgAddPropertyVarSet.ui | 126 --- src/Gui/Dialogs/DlgExpressionInput.cpp | 4 +- src/Gui/Dialogs/DlgExpressionInput.h | 2 +- src/Gui/ViewProviderVarSet.cpp | 2 +- src/Gui/ViewProviderVarSet.h | 4 +- 11 files changed, 1174 insertions(+), 1500 deletions(-) delete mode 100644 src/Gui/Dialogs/DlgAddPropertyVarSet.cpp delete mode 100644 src/Gui/Dialogs/DlgAddPropertyVarSet.h delete mode 100644 src/Gui/Dialogs/DlgAddPropertyVarSet.ui diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 1543a135e9..d70d360f16 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -471,7 +471,6 @@ SET(Gui_UIC_SRCS TaskElementColors.ui Dialogs/DlgObjectSelection.ui Dialogs/DlgAddProperty.ui - Dialogs/DlgAddPropertyVarSet.ui VectorListEditor.ui ) @@ -563,7 +562,6 @@ SET(Dialog_CPP_SRCS TaskElementColors.cpp Dialogs/DlgObjectSelection.cpp Dialogs/DlgAddProperty.cpp - Dialogs/DlgAddPropertyVarSet.cpp VectorListEditor.cpp ) @@ -605,7 +603,6 @@ SET(Dialog_HPP_SRCS TaskElementColors.h Dialogs/DlgObjectSelection.h Dialogs/DlgAddProperty.h - Dialogs/DlgAddPropertyVarSet.h VectorListEditor.h ) diff --git a/src/Gui/Dialogs/DlgAddProperty.cpp b/src/Gui/Dialogs/DlgAddProperty.cpp index 963a4cb295..8f0ae2b1f6 100644 --- a/src/Gui/Dialogs/DlgAddProperty.cpp +++ b/src/Gui/Dialogs/DlgAddProperty.cpp @@ -1,165 +1,932 @@ -/**************************************************************************** - * Copyright (c) 2019 Zheng Lei (realthunder) * - * * - * This file is part of the FreeCAD CAx development system. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Library General Public * - * License as published by the Free Software Foundation; either * - * version 2 of the License, or (at your option) any later version. * - * * - * This library 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 library; see the file COPYING.LIB. If not, * - * write to the Free Software Foundation, Inc., 59 Temple Place, * - * Suite 330, Boston, MA 02111-1307, USA * - * * - ****************************************************************************/ - -#include "PreCompiled.h" -#ifndef _PreComp_ -# include -#endif - -#include -#include -#include -#include -#include - -#include "Dialogs/DlgAddProperty.h" -#include "ui_DlgAddProperty.h" -#include "MainWindow.h" -#include "ViewProviderDocumentObject.h" - - -using namespace Gui; -using namespace Gui::Dialog; - -DlgAddProperty::DlgAddProperty(QWidget* parent, - std::unordered_set &&c) - : QDialog( parent ) - , containers(std::move(c)) - , ui(new Ui_DlgAddProperty) -{ - ui->setupUi(this); - - auto hGrp = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/PropertyView"); - auto defType = Base::Type::fromName( - hGrp->GetASCII("NewPropertyType","App::PropertyString").c_str()); - if(defType.isBad()) - defType = App::PropertyString::getClassTypeId(); - - std::vector proptypes; - std::vector types; - Base::Type::getAllDerivedFrom(Base::Type::fromName("App::Property"), proptypes); - std::copy_if (proptypes.begin(), proptypes.end(), std::back_inserter(types), [](const Base::Type& type) { - return type.canInstantiate(); - }); - std::sort(types.begin(), types.end(), [](Base::Type a, Base::Type b) { - return strcmp(a.getName(), b.getName()) < 0; - }); - - for(const auto& type : types) { - ui->comboType->addItem(QString::fromLatin1(type.getName())); - if(type == defType) - ui->comboType->setCurrentIndex(ui->comboType->count()-1); - } - - ui->edtGroup->setText(QString::fromLatin1( - hGrp->GetASCII("NewPropertyGroup","Base").c_str())); - ui->chkAppend->setChecked(hGrp->GetBool("NewPropertyAppend",true)); -} - -/** - * Destroys the object and frees any allocated resources - */ -DlgAddProperty::~DlgAddProperty() = default; - -static std::string containerName(const App::PropertyContainer *c) { - auto doc = freecad_cast(c); - if(doc) - return doc->getName(); - auto obj = freecad_cast(c); - if(obj) - return obj->getFullName(); - auto vpd = freecad_cast(c); - if(vpd) - return vpd->getObject()->getFullName(); - return "?"; -} - -void DlgAddProperty::accept() -{ - std::string name = ui->edtName->text().toUtf8().constData(); - std::string group = ui->edtGroup->text().toUtf8().constData(); - if(name.empty() || group.empty() - || name != Base::Tools::getIdentifier(name) - || group != Base::Tools::getIdentifier(group)) - { - QMessageBox::critical(getMainWindow(), - QObject::tr("Invalid name"), - QObject::tr("The property or group name must only contain alphanumericals,\n" - "underscore, and must not start with a digit.")); - return; - } - - if(ui->chkAppend->isChecked()) - name = group + "_" + name; - - if (App::ExpressionParser::isTokenAUnit(name) || App::ExpressionParser::isTokenAConstant(name)) { - QMessageBox::critical(getMainWindow(), - QObject::tr("Invalid name"), - QObject::tr("The property name is a reserved word.")); - return; - } - - for(auto c : containers) { - auto prop = c->getPropertyByName(name.c_str()); - if(prop && prop->getContainer() == c) { - QMessageBox::critical(getMainWindow(), - QObject::tr("Invalid name"), - QObject::tr("The property '%1' already exists in '%2'").arg( - QString::fromLatin1(name.c_str()), - QString::fromLatin1(containerName(c).c_str()))); - return; - } - } - - std::string type = ui->comboType->currentText().toLatin1().constData(); - - for(auto it=containers.begin();it!=containers.end();++it) { - try { - (*it)->addDynamicProperty(type.c_str(),name.c_str(), - group.c_str(),ui->edtDoc->toPlainText().toUtf8().constData()); - } catch(Base::Exception &e) { - e.reportException(); - for(auto it2=containers.begin();it2!=it;++it2) { - try { - (*it2)->removeDynamicProperty(name.c_str()); - } catch(Base::Exception &e) { - e.reportException(); - } - } - QMessageBox::critical(getMainWindow(), - QObject::tr("Add property"), - QObject::tr("Failed to add property to '%1': %2").arg( - QString::fromLatin1(containerName(*it).c_str()), - QString::fromUtf8(e.what()))); - return; - } - } - auto hGrp = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/PropertyView"); - hGrp->SetASCII("NewPropertyType",type.c_str()); - hGrp->SetASCII("NewPropertyGroup",group.c_str()); - hGrp->SetBool("NewPropertyAppend",ui->chkAppend->isChecked()); - QDialog::accept(); -} - -#include "moc_DlgAddProperty.cpp" +// 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 "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +# include +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Dialogs/DlgAddProperty.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), and + * - 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); + + initializeWidgets(viewProvider); +} + +DlgAddProperty::~DlgAddProperty() = default; + +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); +} + +std::vector DlgAddProperty::getSupportedTypes() +{ + std::vector supportedTypes; + std::vector allTypes; + Base::Type::getAllDerivedFrom(Base::Type::fromName("App::Property"), allTypes); + + std::ranges::copy_if(allTypes, std::back_inserter(supportedTypes), + [](const Base::Type& type) { + return type.canInstantiate(); + }); + + std::ranges::sort(supportedTypes, [](Base::Type a, Base::Type b) { + return strcmp(a.getName(), b.getName()) < 0; + }); + + return supportedTypes; +} + +void DlgAddProperty::initializeTypes() +{ + 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(); + } + + std::vector types = getSupportedTypes(); + + for(const auto& type : types) { + ui->comboBoxType->addItem(QString::fromLatin1(type.getName())); + if (type == lastType) { + ui->comboBoxType->setCurrentIndex(ui->comboBoxType->count()-1); + } + } + + 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 (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->comboBoxType, editor.get()); + QWidget::setTabOrder(editor.get(), ui->checkBoxAdd); + + removeSelectionEditor(); +} + +bool DlgAddProperty::isTypeWithEditor(const Base::Type& type) +{ + 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. + App::PropertyEnumeration::getClassTypeId(), + App::PropertyFile::getClassTypeId(), + App::PropertyFloatList::getClassTypeId(), + App::PropertyFont::getClassTypeId(), + App::PropertyIntegerList::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) +{ + 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::createEditorForType(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; + } + + if (isTypeWithEditor(propType)) { + createEditorForType(propType); + } + else { + removeEditor(); + } +} + +void DlgAddProperty::setTitle() +{ + setWindowTitle(tr("Add Property")); +} + +void DlgAddProperty::setOkEnabled(bool enabled) +{ + QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + okButton->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(); + setOkEnabled(false); + + ui->lineEditName->setFocus(); + + QWidget::setTabOrder(ui->lineEditName, &comboBoxGroup); + QWidget::setTabOrder(&comboBoxGroup, ui->comboBoxType); +} + +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()); +} + +bool DlgAddProperty::isDocument() const +{ + return container && freecad_cast(container); +} + +bool DlgAddProperty::isDocumentObject() const +{ + return container && freecad_cast(container); +} + +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->comboBoxType, ui->checkBoxAdd); + 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; + } + + setOkEnabled(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()); + } + } + + setOkEnabled(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 + + setOkEnabled(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) { + criticalDialogShown = true; + QMessageBox::critical(this, title, text); + criticalDialogShown = false; + } +} + +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(); + + try { + return container->addDynamicProperty(type.c_str(), name.c_str(), + group.c_str(), doc.c_str()); + } + 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(); + setOkEnabled(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); + 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()); + + if (ui->checkBoxAdd->isChecked()) { + clearFields(); + ui->lineEditName->setFocus(); + } + else { + // we are done, close the dialog + QDialog::accept(); + } +} + +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" diff --git a/src/Gui/Dialogs/DlgAddProperty.h b/src/Gui/Dialogs/DlgAddProperty.h index 4a26faa6c1..1ac0a598cc 100644 --- a/src/Gui/Dialogs/DlgAddProperty.h +++ b/src/Gui/Dialogs/DlgAddProperty.h @@ -1,56 +1,189 @@ -/**************************************************************************** - * Copyright (c) 2019 Zheng Lei (realthunder) * - * * - * This file is part of the FreeCAD CAx development system. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Library General Public * - * License as published by the Free Software Foundation; either * - * version 2 of the License, or (at your option) any later version. * - * * - * This library 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 library; see the file COPYING.LIB. If not, * - * write to the Free Software Foundation, Inc., 59 Temple Place, * - * Suite 330, Boston, MA 02111-1307, USA * - * * - ****************************************************************************/ - -#ifndef GUI_DIALOG_DLGADDPROPERTY_H -#define GUI_DIALOG_DLGADDPROPERTY_H - -#include -#include -#include - -namespace App { -class PropertyContainer; -} - -namespace Gui { -namespace Dialog { - -class Ui_DlgAddProperty; -class GuiExport DlgAddProperty : public QDialog -{ - Q_OBJECT - -public: - DlgAddProperty(QWidget *parent, std::unordered_set &&); - ~DlgAddProperty() override; - - void accept() override; - -private: - std::unordered_set containers; - std::unique_ptr ui; -}; - -} // namespace Dialog -} // namespace Gui - -#endif // GUI_DIALOG_DLGADDPROPERTY_H +// 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 * + * . * + * * + ***************************************************************************/ + +#ifndef GUI_DIALOG_DLG_ADD_PROPERTY_H +#define GUI_DIALOG_DLG_ADD_PROPERTY_H + +#include + +#include +#include +#include + +#include + +#include + +#include "propertyeditor/PropertyItem.h" + +namespace Gui { + +class ViewProviderVarSet; + +namespace Dialog { + +class EditFinishedComboBox : public QComboBox { + Q_OBJECT +public: + explicit EditFinishedComboBox(QWidget *parent = nullptr) : QComboBox(parent) { + setEditable(true); + connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, &EditFinishedComboBox::onIndexChanged); + connect(this->lineEdit(), &QLineEdit::editingFinished, this, &EditFinishedComboBox::onEditingFinished); + } + +Q_SIGNALS: + void editFinished(); + +private: + void onEditingFinished() { + Q_EMIT editFinished(); + } + + void onIndexChanged() { + Q_EMIT editFinished(); + } +}; + +class Ui_DlgAddProperty; + +class GuiExport DlgAddProperty : public QDialog +{ + Q_OBJECT + +public: + static const std::string GroupBase; + +public: + DlgAddProperty(QWidget* parent, ViewProviderVarSet* viewProvider); + DlgAddProperty(QWidget* parent, App::PropertyContainer* container); + + DlgAddProperty(const DlgAddProperty&) = delete; + DlgAddProperty(DlgAddProperty&&) = delete; + DlgAddProperty& operator=(const DlgAddProperty&) = delete; + DlgAddProperty& operator=(DlgAddProperty&&) = delete; + + ~DlgAddProperty() override; + + void changeEvent(QEvent* e) override; + void accept() override; + void reject() override; + static void populateGroup(EditFinishedComboBox& comboBox, + const App::PropertyContainer* container); + static void setWidgetForLabel(const char* labelName, QWidget* widget, + QLayout* layout); + +public Q_SLOTS: + void valueChanged(); + void valueChangedEnum(); + +private: + enum class TransactionOption : bool { + Commit = false, + Abort = true + }; + + enum class FieldChange : std::uint8_t { + Name, + Type + }; + + DlgAddProperty(QWidget* parent, App::PropertyContainer* container, + ViewProviderVarSet* viewProvider); + + void initializeGroup(); + + std::vector getSupportedTypes(); + void initializeTypes(); + + void removeSelectionEditor(); + QVariant getEditorData() const; + void setEditorData(const QVariant& data); + bool isEnumPropertyItem() const; + void addEnumEditor(PropertyEditor::PropertyItem* propertyItem); + void addNormalEditor(PropertyEditor::PropertyItem* propertyItem); + void addEditor(PropertyEditor::PropertyItem* propertyItem); + bool isTypeWithEditor(const Base::Type& type); + bool isTypeWithEditor(const std::string& type); + void createEditorForType(const Base::Type& type); + void initializeValue(); + + void setTitle(); + void setOkEnabled(bool enabled); + void initializeWidgets(ViewProviderVarSet* viewProvider); + + bool isDocument() const; + bool isDocumentObject() const; + bool propertyExists(const std::string& name); + bool isNameValid(); + bool isGroupValid(); + bool isTypeValid(); + bool areFieldsValid(); + + void setEditor(bool valueNeedsReset); + void buildForUnbound(bool valueNeedsReset); + void setPropertyItem(App::Property* prop, bool supportsExpressions); + void buildForBound(bool valueNeedsReset, bool supportsExpressions); + bool clearBoundProperty(); + bool clear(FieldChange fieldChange); + void onNameChanged(const QString& text); + void onGroupFinished(); + void onTypeChanged(const QString& text); + + void showStatusMessage(); + + void removeEditor(); + + void openTransaction(); + void critical(const QString& title, const QString& text); + App::Property* createProperty(); + void closeTransaction(TransactionOption option); + void clearFields(); + void addDocumentation(); + + static void removeExistingWidget(QFormLayout* layout, int labelRow); + static int findLabelRow(const char* labelName, QFormLayout* layout); + +private: + App::PropertyContainer* container; + std::unique_ptr ui; + + EditFinishedComboBox comboBoxGroup; + QCompleter completerType; + + std::unique_ptr editor; + std::unique_ptr propertyItem; + std::unique_ptr objectIdentifier; + + // a transactionID of 0 means that there is no active transaction. + int transactionID; + + QMetaObject::Connection connComboBoxGroup; + QMetaObject::Connection connComboBoxType; + QMetaObject::Connection connLineEditNameTextChanged; +}; + +} // namespace Dialog +} // namespace Gui + +#endif // GUI_DIALOG_DLG_ADD_PROPERTY_H diff --git a/src/Gui/Dialogs/DlgAddProperty.ui b/src/Gui/Dialogs/DlgAddProperty.ui index a80dd8036b..57e006e93a 100644 --- a/src/Gui/Dialogs/DlgAddProperty.ui +++ b/src/Gui/Dialogs/DlgAddProperty.ui @@ -7,7 +7,7 @@ 0 0 418 - 258 + 293 @@ -15,83 +15,79 @@ - - - Type - - - - - - - - - - Group - - - - - - - - + Name - - + + - - - - Verbose description of the new property - + + - Documentation + Group - - - - Verbose description of the new property + + + + Type + + + + + + + true + + + + + + + Value - - - Prefix the property name with the group name in the form 'Group_Name' to avoid conflicts with an existing property. -In this case the prefix will be automatically trimmed when shown in the property editor. -However, the property is still used in a script with the full name, like 'obj.Group_Name'. - -If this is not checked, the property must be uniquely named, and it is accessed like 'obj.Name'. - + - Prefix group name + Add another - + + + + Tooltip + + + + + + + + + + + + + + - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok - - comboType - edtGroup - edtName - diff --git a/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp b/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp deleted file mode 100644 index d62b3c18dd..0000000000 --- a/src/Gui/Dialogs/DlgAddPropertyVarSet.cpp +++ /dev/null @@ -1,908 +0,0 @@ -/**************************************************************************** - * Copyright (c) 2024 Ondsel * - * Copyright (c) 2025 Pieter Hijma * - * * - * This file is part of the FreeCAD CAx development system. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Library General Public * - * License as published by the Free Software Foundation; either * - * version 2 of the License, or (at your option) any later version. * - * * - * This library 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 library; see the file COPYING.LIB. If not, * - * write to the Free Software Foundation, Inc., 59 Temple Place, * - * Suite 330, Boston, MA 02111-1307, USA * - * * - ****************************************************************************/ - -#include "PreCompiled.h" -#ifndef _PreComp_ -# include -# include -# include -# include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Dialogs/DlgAddPropertyVarSet.h" -#include "ui_DlgAddPropertyVarSet.h" -#include "ViewProviderVarSet.h" -#include "propertyeditor/PropertyItem.h" - -FC_LOG_LEVEL_INIT("DlgAddPropertyVarSet", true, true) - -using namespace Gui; -using namespace Gui::Dialog; -using namespace Gui::PropertyEditor; - -const std::string DlgAddPropertyVarSet::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), and - * - 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. - */ - -DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent, - App::PropertyContainer* container) - : DlgAddPropertyVarSet(parent, container, nullptr) -{ -} - - -DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent, - ViewProviderVarSet* viewProvider) - : DlgAddPropertyVarSet(parent, - viewProvider ? viewProvider->getObject() : nullptr, - viewProvider) -{ -} - -DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent, - App::PropertyContainer* container, - ViewProviderVarSet* viewProvider) - : QDialog(parent), - container(container), - ui(new Ui_DlgAddPropertyVarSet), - comboBoxGroup(this), - completerType(this), - editor(nullptr), - transactionID(0) -{ - ui->setupUi(this); - - initializeWidgets(viewProvider); -} - -DlgAddPropertyVarSet::~DlgAddPropertyVarSet() = default; - -int DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::removeExistingWidget(QFormLayout* formLayout, int labelRow) -{ - if (QLayoutItem* existingItem = formLayout->itemAt(labelRow, QFormLayout::FieldRole)) { - if (QWidget *existingWidget = existingItem->widget()) { - formLayout->removeWidget(existingWidget); - existingWidget->deleteLater(); - } - } -} - -void DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::populateGroup(EditFinishedComboBox& comboBox, - const App::PropertyContainer* container) -{ - 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)); - } - - comboBox.setEditText(QString::fromStdString(groupNamesSorted[0])); -} - -void DlgAddPropertyVarSet::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, &DlgAddPropertyVarSet::onGroupFinished); -} - -std::vector DlgAddPropertyVarSet::getSupportedTypes() -{ - std::vector supportedTypes; - std::vector allTypes; - Base::Type::getAllDerivedFrom(Base::Type::fromName("App::Property"), allTypes); - - std::ranges::copy_if(allTypes, std::back_inserter(supportedTypes), - [](const Base::Type& type) { - return type.canInstantiate(); - }); - - std::ranges::sort(supportedTypes, [](Base::Type a, Base::Type b) { - return strcmp(a.getName(), b.getName()) < 0; - }); - - return supportedTypes; -} - -void DlgAddPropertyVarSet::initializeTypes() -{ - 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(); - } - - std::vector types = getSupportedTypes(); - - for(const auto& type : types) { - ui->comboBoxType->addItem(QString::fromLatin1(type.getName())); - if (type == lastType) { - ui->comboBoxType->setCurrentIndex(ui->comboBoxType->count()-1); - } - } - - 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, &DlgAddPropertyVarSet::onTypeChanged); -} - -void DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::addNormalEditor(PropertyItem* propertyItem) -{ - editor.reset(propertyItem->createEditor(this, [this]() { - this->valueChanged(); - }, FrameOption::WithFrame)); -} - -void DlgAddPropertyVarSet::addEditor(PropertyItem* propertyItem) -{ - 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->comboBoxType, editor.get()); - QWidget::setTabOrder(editor.get(), ui->checkBoxAdd); - - removeSelectionEditor(); -} - -bool DlgAddPropertyVarSet::isTypeWithEditor(const Base::Type& type) -{ - 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. - App::PropertyEnumeration::getClassTypeId(), - App::PropertyFile::getClassTypeId(), - App::PropertyFloatList::getClassTypeId(), - App::PropertyFont::getClassTypeId(), - App::PropertyIntegerList::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 DlgAddPropertyVarSet::isTypeWithEditor(const std::string& type) -{ - 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 DlgAddPropertyVarSet::createEditorForType(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 DlgAddPropertyVarSet::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; - } - - if (isTypeWithEditor(propType)) { - createEditorForType(propType); - } - else { - removeEditor(); - } -} - -void DlgAddPropertyVarSet::setTitle() -{ - setWindowTitle(tr("Add Property")); -} - -void DlgAddPropertyVarSet::setOkEnabled(bool enabled) -{ - QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok); - okButton->setEnabled(enabled); -} - -void DlgAddPropertyVarSet::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, &DlgAddPropertyVarSet::onNameChanged); - - setTitle(); - setOkEnabled(false); - - ui->lineEditName->setFocus(); - - QWidget::setTabOrder(ui->lineEditName, &comboBoxGroup); - QWidget::setTabOrder(&comboBoxGroup, ui->comboBoxType); -} - -bool DlgAddPropertyVarSet::propertyExists(const std::string& name) -{ - App::Property* prop = container->getPropertyByName(name.c_str()); - return prop && prop->getContainer() == container && - !(propertyItem && propertyItem->getFirstProperty() == prop); -} - -bool DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::isGroupValid() -{ - std::string group = comboBoxGroup.currentText().toStdString(); - return !group.empty() && group == Base::Tools::getIdentifier(group); -} - -bool DlgAddPropertyVarSet::isTypeValid() -{ - std::string type = ui->comboBoxType->currentText().toStdString(); - return Base::Type::fromName(type.c_str()).isDerivedFrom(App::Property::getClassTypeId()); -} - -bool DlgAddPropertyVarSet::areFieldsValid() -{ - return isNameValid() && isGroupValid() && isTypeValid(); -} - -void DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::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->comboBoxType, ui->checkBoxAdd); - editor = nullptr; -} - -bool DlgAddPropertyVarSet::isEnumPropertyItem() const -{ - return ui->comboBoxType->currentText() == - QString::fromLatin1(App::PropertyEnumeration::getClassTypeId().getName()); -} - -QVariant DlgAddPropertyVarSet::getEditorData() const -{ - if (isEnumPropertyItem()) { - PropertyItem* child = propertyItem->child(0); - if (child == nullptr) { - return {}; - } - return child->editorData(editor.get()); - } - - return propertyItem->editorData(editor.get()); -} - -void DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::setPropertyItem(App::Property* prop) -{ - if (prop == nullptr) { - return; - } - - if (propertyItem == nullptr) { - propertyItem.reset(createPropertyItem(prop)); - } - - if (propertyItem == nullptr) { - return; - } - - objectIdentifier = std::make_unique(*prop); - propertyItem->setAutoApply(true); - propertyItem->setPropertyData({prop}); - propertyItem->bind(*objectIdentifier); -} - -void DlgAddPropertyVarSet::buildForUnbound(bool valueNeedsReset) -{ - setEditor(valueNeedsReset); -} - -void DlgAddPropertyVarSet::buildForBound(bool valueNeedsReset) -{ - openTransaction(); - App::Property* prop = createProperty(); - setPropertyItem(prop); - setEditor(valueNeedsReset); -} - -bool DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::clear(FieldChange fieldChange) -{ - if (propertyItem == nullptr) { - return true; - } - - bool valueNeedsReset = clearBoundProperty(); - - if (fieldChange == FieldChange::Type) { - valueNeedsReset = true; - removeEditor(); - propertyItem = nullptr; - } - return valueNeedsReset; -} - -void DlgAddPropertyVarSet::onNameChanged([[maybe_unused]]const QString& text) -{ - bool valueNeedsReset = clear(FieldChange::Name); - if (isNameValid() && isTypeValid()) { - buildForBound(valueNeedsReset); - } - else if (isTypeValid()) { - buildForUnbound(valueNeedsReset); - } - else { - removeEditor(); - propertyItem = nullptr; - } - - setOkEnabled(areFieldsValid()); - showStatusMessage(); -} - -void DlgAddPropertyVarSet::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()); - } - } - - setOkEnabled(areFieldsValid()); - showStatusMessage(); -} - -void DlgAddPropertyVarSet::onTypeChanged([[maybe_unused]] const QString& text) -{ - bool valueNeedsReset = clear(FieldChange::Type); - if (isNameValid() && isTypeValid()) { - buildForBound(valueNeedsReset); - } - else if (isTypeValid()) { - buildForUnbound(valueNeedsReset); - } - // nothing if both name and type are invalid - - setOkEnabled(areFieldsValid()); - showStatusMessage(); -} - -void DlgAddPropertyVarSet::changeEvent(QEvent* e) -{ - if (e->type() == QEvent::LanguageChange) { - ui->retranslateUi(this); - setTitle(); - } - QDialog::changeEvent(e); -} - -void DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::openTransaction() -{ - transactionID = App::GetApplication().setActiveTransaction("Add property VarSet"); -} - -void DlgAddPropertyVarSet::critical(const QString& title, const QString& text) { - static bool criticalDialogShown = false; - if (!criticalDialogShown) { - criticalDialogShown = true; - QMessageBox::critical(this, title, text); - criticalDialogShown = false; - } -} - -App::Property* DlgAddPropertyVarSet::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(); - - try { - return container->addDynamicProperty(type.c_str(), name.c_str(), - group.c_str(), doc.c_str()); - } - 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 DlgAddPropertyVarSet::closeTransaction(TransactionOption option) -{ - if (transactionID == 0) { - return; - } - - App::GetApplication().closeActiveTransaction(static_cast(option), transactionID); - transactionID = 0; -} - -void DlgAddPropertyVarSet::clearFields() -{ - { - QSignalBlocker blocker(ui->lineEditName); - ui->lineEditName->clear(); - } - ui->lineEditToolTip->clear(); - initializeValue(); - setOkEnabled(false); -} - -void DlgAddPropertyVarSet::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 DlgAddPropertyVarSet::accept() -{ - addDocumentation(); - auto* object = freecad_cast(container); - if (object) { - object->ExpressionEngine.execute(); - } - closeTransaction(TransactionOption::Commit); - 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()); - - if (ui->checkBoxAdd->isChecked()) { - clearFields(); - ui->lineEditName->setFocus(); - } - else { - // we are done, close the dialog - QDialog::accept(); - } -} - -void DlgAddPropertyVarSet::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_DlgAddPropertyVarSet.cpp" diff --git a/src/Gui/Dialogs/DlgAddPropertyVarSet.h b/src/Gui/Dialogs/DlgAddPropertyVarSet.h deleted file mode 100644 index 60b1177af8..0000000000 --- a/src/Gui/Dialogs/DlgAddPropertyVarSet.h +++ /dev/null @@ -1,185 +0,0 @@ -/**************************************************************************** - * Copyright (c) 2024 Ondsel * - * Copyright (c) 2025 Pieter Hijma * - * * - * This file is part of the FreeCAD CAx development system. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Library General Public * - * License as published by the Free Software Foundation; either * - * version 2 of the License, or (at your option) any later version. * - * * - * This library 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 library; see the file COPYING.LIB. If not, * - * write to the Free Software Foundation, Inc., 59 Temple Place, * - * Suite 330, Boston, MA 02111-1307, USA * - * * - ****************************************************************************/ - -#ifndef GUI_DIALOG_DLG_ADD_PROPERTY_VARSET_H -#define GUI_DIALOG_DLG_ADD_PROPERTY_VARSET_H - -#include - -#include -#include -#include - -#include - -#include - -#include "propertyeditor/PropertyItem.h" - -namespace Gui { - -class ViewProviderVarSet; - -namespace Dialog { - -class EditFinishedComboBox : public QComboBox { - Q_OBJECT -public: - explicit EditFinishedComboBox(QWidget *parent = nullptr) : QComboBox(parent) { - setEditable(true); - connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, &EditFinishedComboBox::onIndexChanged); - connect(this->lineEdit(), &QLineEdit::editingFinished, this, &EditFinishedComboBox::onEditingFinished); - } - -Q_SIGNALS: - void editFinished(); - -private: - void onEditingFinished() { - Q_EMIT editFinished(); - } - - void onIndexChanged() { - Q_EMIT editFinished(); - } -}; - -class Ui_DlgAddPropertyVarSet; - -class GuiExport DlgAddPropertyVarSet : public QDialog -{ - Q_OBJECT - -public: - static const std::string GroupBase; - -public: - DlgAddPropertyVarSet(QWidget* parent, ViewProviderVarSet* viewProvider); - DlgAddPropertyVarSet(QWidget* parent, App::PropertyContainer* container); - - DlgAddPropertyVarSet(const DlgAddPropertyVarSet&) = delete; - DlgAddPropertyVarSet(DlgAddPropertyVarSet&&) = delete; - DlgAddPropertyVarSet& operator=(const DlgAddPropertyVarSet&) = delete; - DlgAddPropertyVarSet& operator=(DlgAddPropertyVarSet&&) = delete; - - ~DlgAddPropertyVarSet() override; - - void changeEvent(QEvent* e) override; - void accept() override; - void reject() override; - static void populateGroup(EditFinishedComboBox& comboBox, - const App::PropertyContainer* container); - static void setWidgetForLabel(const char* labelName, QWidget* widget, QLayout* layout); - -public Q_SLOTS: - void valueChanged(); - void valueChangedEnum(); - -private: - enum class TransactionOption : bool { - Commit = false, - Abort = true - }; - - enum class FieldChange : std::uint8_t { - Name, - Type - }; - - DlgAddPropertyVarSet(QWidget* parent, App::PropertyContainer* container, - ViewProviderVarSet* viewProvider); - - void initializeGroup(); - - std::vector getSupportedTypes(); - void initializeTypes(); - - void removeSelectionEditor(); - QVariant getEditorData() const; - void setEditorData(const QVariant& data); - bool isEnumPropertyItem() const; - void addEnumEditor(PropertyEditor::PropertyItem* propertyItem); - void addNormalEditor(PropertyEditor::PropertyItem* propertyItem); - void addEditor(PropertyEditor::PropertyItem* propertyItem); - bool isTypeWithEditor(const Base::Type& type); - bool isTypeWithEditor(const std::string& type); - void createEditorForType(const Base::Type& type); - void initializeValue(); - - void setTitle(); - void setOkEnabled(bool enabled); - void initializeWidgets(ViewProviderVarSet* viewProvider); - - bool propertyExists(const std::string& name); - bool isNameValid(); - bool isGroupValid(); - bool isTypeValid(); - bool areFieldsValid(); - - void setEditor(bool valueNeedsReset); - void buildForUnbound(bool valueNeedsReset); - void setPropertyItem(App::Property* prop); - void buildForBound(bool valueNeedsReset); - bool clearBoundProperty(); - bool clear(FieldChange fieldChange); - void onNameChanged(const QString& text); - void onGroupFinished(); - void onTypeChanged(const QString& text); - - void showStatusMessage(); - - void removeEditor(); - - void openTransaction(); - void critical(const QString& title, const QString& text); - App::Property* createProperty(); - void closeTransaction(TransactionOption option); - void clearFields(); - void addDocumentation(); - - static void removeExistingWidget(QFormLayout* layout, int labelRow); - static int findLabelRow(const char* labelName, QFormLayout* layout); - -private: - App::PropertyContainer* container; - std::unique_ptr ui; - - EditFinishedComboBox comboBoxGroup; - QCompleter completerType; - - std::unique_ptr editor; - std::unique_ptr propertyItem; - std::unique_ptr objectIdentifier; - - // a transactionID of 0 means that there is no active transaction. - int transactionID; - - QMetaObject::Connection connComboBoxGroup; - QMetaObject::Connection connComboBoxType; - QMetaObject::Connection connLineEditNameTextChanged; -}; - -} // namespace Dialog -} // namespace Gui - -#endif // GUI_DIALOG_DLG_ADD_PROPERTY_VARSET_H diff --git a/src/Gui/Dialogs/DlgAddPropertyVarSet.ui b/src/Gui/Dialogs/DlgAddPropertyVarSet.ui deleted file mode 100644 index b662a108e9..0000000000 --- a/src/Gui/Dialogs/DlgAddPropertyVarSet.ui +++ /dev/null @@ -1,126 +0,0 @@ - - - Gui::Dialog::DlgAddPropertyVarSet - - - - 0 - 0 - 418 - 234 - - - - Add Property - - - - - - Name - - - - - - - - - - Group - - - - - - - Type - - - - - - - true - - - - - - - Value - - - - - - - Add another - - - - - - - Tooltip - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - Gui::Dialog::DlgAddPropertyVarSet - accept() - - - 199 - 99 - - - 199 - 58 - - - - - buttonBox - rejected() - Gui::Dialog::DlgAddPropertyVarSet - reject() - - - 199 - 99 - - - 199 - 58 - - - - - diff --git a/src/Gui/Dialogs/DlgExpressionInput.cpp b/src/Gui/Dialogs/DlgExpressionInput.cpp index 78c892f39b..6307db5736 100644 --- a/src/Gui/Dialogs/DlgExpressionInput.cpp +++ b/src/Gui/Dialogs/DlgExpressionInput.cpp @@ -237,7 +237,7 @@ void DlgExpressionInput::initializeVarSets() comboBoxGroup.setObjectName(QStringLiteral("comboBoxGroup")); comboBoxGroup.setInsertPolicy(QComboBox::InsertAtTop); comboBoxGroup.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - DlgAddPropertyVarSet::setWidgetForLabel("labelGroup", &comboBoxGroup, ui->formLayout); + DlgAddProperty::setWidgetForLabel("labelGroup", &comboBoxGroup, ui->formLayout); setTabOrder(ui->comboBoxVarSet, &comboBoxGroup); setTabOrder(&comboBoxGroup, ui->lineEditPropNew); @@ -819,7 +819,7 @@ void DlgExpressionInput::onVarSetSelected(int /*index*/) return; } - DlgAddPropertyVarSet::populateGroup(comboBoxGroup, varSet); + DlgAddProperty::populateGroup(comboBoxGroup, varSet); preselectGroup(); updateVarSetInfo(); ui->lineEditPropNew->setFocus(); diff --git a/src/Gui/Dialogs/DlgExpressionInput.h b/src/Gui/Dialogs/DlgExpressionInput.h index ce62aa2d95..aeb152e2d0 100644 --- a/src/Gui/Dialogs/DlgExpressionInput.h +++ b/src/Gui/Dialogs/DlgExpressionInput.h @@ -31,7 +31,7 @@ #include #include -#include "Dialogs/DlgAddPropertyVarSet.h" +#include "Dialogs/DlgAddProperty.h" namespace Ui { class DlgExpressionInput; diff --git a/src/Gui/ViewProviderVarSet.cpp b/src/Gui/ViewProviderVarSet.cpp index d9179422d5..e43af5873a 100644 --- a/src/Gui/ViewProviderVarSet.cpp +++ b/src/Gui/ViewProviderVarSet.cpp @@ -44,7 +44,7 @@ ViewProviderVarSet::ViewProviderVarSet() bool ViewProviderVarSet::doubleClicked() { if (!dialog) { - dialog = std::make_unique(getMainWindow(), this); + dialog = std::make_unique(getMainWindow(), this); } // Do not use exec() here because it blocks and prevents command Std_VarSet diff --git a/src/Gui/ViewProviderVarSet.h b/src/Gui/ViewProviderVarSet.h index 6afa7b975e..3542abedfc 100644 --- a/src/Gui/ViewProviderVarSet.h +++ b/src/Gui/ViewProviderVarSet.h @@ -24,7 +24,7 @@ #define GUI_ViewProviderVarSet_H #include "ViewProviderDocumentObject.h" -#include "Dialogs/DlgAddPropertyVarSet.h" +#include "Dialogs/DlgAddProperty.h" namespace Gui { @@ -44,7 +44,7 @@ public: void onFinished(int); private: - std::unique_ptr dialog; + std::unique_ptr dialog; }; } // namespace Gui From 550aac53a779bba2352f1cda961af1f133906a0f Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Wed, 3 Sep 2025 11:08:55 +0200 Subject: [PATCH 5/9] Gui: Improve adding multiple properties This is based on a review of the DWG suggesting to remove the checkbox for adding multiple properties and change the Ok button to an Add button. To make the impact of an extra click to cancel adding properties, the Cancel button is the default right after adding a property (which already disabled the Ok/Add button). --- src/Gui/Dialogs/DlgAddProperty.cpp | 44 ++++++++++++++++++------------ src/Gui/Dialogs/DlgAddProperty.h | 2 +- src/Gui/Dialogs/DlgAddProperty.ui | 17 ++++-------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/Gui/Dialogs/DlgAddProperty.cpp b/src/Gui/Dialogs/DlgAddProperty.cpp index 8f0ae2b1f6..fe7a827188 100644 --- a/src/Gui/Dialogs/DlgAddProperty.cpp +++ b/src/Gui/Dialogs/DlgAddProperty.cpp @@ -373,7 +373,7 @@ void DlgAddProperty::addEditor(PropertyItem* propertyItem) setWidgetForLabel("labelValue", editor.get(), layout()); QWidget::setTabOrder(ui->comboBoxType, editor.get()); - QWidget::setTabOrder(editor.get(), ui->checkBoxAdd); + QWidget::setTabOrder(editor.get(), ui->lineEditToolTip); removeSelectionEditor(); } @@ -477,10 +477,19 @@ void DlgAddProperty::setTitle() setWindowTitle(tr("Add Property")); } -void DlgAddProperty::setOkEnabled(bool enabled) +void DlgAddProperty::setAddEnabled(bool enabled) { - QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok); - okButton->setEnabled(enabled); + QPushButton *addButton = ui->buttonBox->button(QDialogButtonBox::Ok); + QPushButton *cancelButton = ui->buttonBox->button(QDialogButtonBox::Cancel); + if (enabled) { + cancelButton->setDefault(false); + addButton->setDefault(true); + } + else { + cancelButton->setDefault(true); + addButton->setDefault(false); + } + addButton->setEnabled(enabled); } void DlgAddProperty::initializeWidgets(ViewProviderVarSet* viewProvider) @@ -497,7 +506,9 @@ void DlgAddProperty::initializeWidgets(ViewProviderVarSet* viewProvider) this, &DlgAddProperty::onNameChanged); setTitle(); - setOkEnabled(false); + QPushButton *addButton = ui->buttonBox->button(QDialogButtonBox::Ok); + addButton->setText(tr("Add")); + setAddEnabled(false); ui->lineEditName->setFocus(); @@ -593,7 +604,7 @@ void DlgAddProperty::removeEditor() placeholder->setMinimumHeight(comboBoxGroup.height()); setWidgetForLabel("labelValue", placeholder, layout()); - QWidget::setTabOrder(ui->comboBoxType, ui->checkBoxAdd); + QWidget::setTabOrder(ui->comboBoxType, ui->lineEditToolTip); editor = nullptr; } @@ -735,7 +746,7 @@ void DlgAddProperty::onNameChanged([[maybe_unused]]const QString& text) propertyItem = nullptr; } - setOkEnabled(areFieldsValid()); + setAddEnabled(areFieldsValid()); showStatusMessage(); } @@ -750,7 +761,7 @@ void DlgAddProperty::onGroupFinished() } } - setOkEnabled(areFieldsValid()); + setAddEnabled(areFieldsValid()); showStatusMessage(); } @@ -765,7 +776,7 @@ void DlgAddProperty::onTypeChanged([[maybe_unused]] const QString& text) } // nothing if both name and type are invalid - setOkEnabled(areFieldsValid()); + setAddEnabled(areFieldsValid()); showStatusMessage(); } @@ -862,7 +873,7 @@ void DlgAddProperty::clearFields() } ui->lineEditToolTip->clear(); initializeValue(); - setOkEnabled(false); + setAddEnabled(false); } void DlgAddProperty::addDocumentation() { @@ -903,14 +914,11 @@ void DlgAddProperty::accept() paramGroup->SetASCII("NewPropertyType", type.c_str()); paramGroup->SetASCII("NewPropertyGroup", group.c_str()); - if (ui->checkBoxAdd->isChecked()) { - clearFields(); - ui->lineEditName->setFocus(); - } - else { - // we are done, close the dialog - QDialog::accept(); - } + 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() diff --git a/src/Gui/Dialogs/DlgAddProperty.h b/src/Gui/Dialogs/DlgAddProperty.h index 1ac0a598cc..c2e0de1b69 100644 --- a/src/Gui/Dialogs/DlgAddProperty.h +++ b/src/Gui/Dialogs/DlgAddProperty.h @@ -129,7 +129,7 @@ private: void initializeValue(); void setTitle(); - void setOkEnabled(bool enabled); + void setAddEnabled(bool enabled); void initializeWidgets(ViewProviderVarSet* viewProvider); bool isDocument() const; diff --git a/src/Gui/Dialogs/DlgAddProperty.ui b/src/Gui/Dialogs/DlgAddProperty.ui index 57e006e93a..a0c00a7d8e 100644 --- a/src/Gui/Dialogs/DlgAddProperty.ui +++ b/src/Gui/Dialogs/DlgAddProperty.ui @@ -7,7 +7,7 @@ 0 0 418 - 293 + 258 @@ -52,31 +52,24 @@ - - - - Add another - - - - + Tooltip - + - + - + Qt::Orientation::Horizontal From 66df6b39c84cbfdbff91bb37fa25c93a36b18fa4 Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Wed, 3 Sep 2025 11:27:53 +0200 Subject: [PATCH 6/9] Gui: Select the VarSet when clicking Std_VarSet To make clear which VarSet is being added to, the VarSet is being selected. --- src/Gui/CommandStructure.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Gui/CommandStructure.cpp b/src/Gui/CommandStructure.cpp index e16fe3421c..2a83205824 100644 --- a/src/Gui/CommandStructure.cpp +++ b/src/Gui/CommandStructure.cpp @@ -183,6 +183,9 @@ void StdCmdVarSet::activated(int iMsg) VarSetName = getUniqueObjectName("VarSet"); doCommand(Doc,"App.activeDocument().addObject('App::VarSet','%s')",VarSetName.c_str()); + Gui::Document* docGui = Application::Instance->activeDocument(); + App::Document* doc = docGui->getDocument(); + // add the varset to a group if it is selected auto sels = Selection().getSelectionEx(nullptr, App::DocumentObject::getClassTypeId(), ResolveMode::OldStyleElement, true); @@ -190,11 +193,14 @@ void StdCmdVarSet::activated(int iMsg) App::DocumentObject* obj = sels[0].getObject(); auto group = obj->getExtension(); if (group) { - Gui::Document* docGui = Application::Instance->activeDocument(); - App::Document* doc = docGui->getDocument(); group->addObject(doc->getObject(VarSetName.c_str())); } } + + // select the new varset + Selection().clearSelection(); + Selection().addSelection(doc->getName(), VarSetName.c_str()); + commitCommand(); doCommand(Doc, "App.ActiveDocument.getObject('%s').ViewObject.doubleClicked()", VarSetName.c_str()); From 459a8bc4c80ed98fddb6f1e9d3db300ccd1941bf Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Wed, 3 Sep 2025 15:33:45 +0200 Subject: [PATCH 7/9] Gui: Adjust size of the Add Property dialog --- src/Gui/Dialogs/DlgAddProperty.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Gui/Dialogs/DlgAddProperty.cpp b/src/Gui/Dialogs/DlgAddProperty.cpp index fe7a827188..c1d80fb90b 100644 --- a/src/Gui/Dialogs/DlgAddProperty.cpp +++ b/src/Gui/Dialogs/DlgAddProperty.cpp @@ -514,6 +514,8 @@ void DlgAddProperty::initializeWidgets(ViewProviderVarSet* viewProvider) QWidget::setTabOrder(ui->lineEditName, &comboBoxGroup); QWidget::setTabOrder(&comboBoxGroup, ui->comboBoxType); + + adjustSize(); } bool DlgAddProperty::propertyExists(const std::string& name) From 12b7699800e0a09f0bbb68f57983f41df7762480 Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Fri, 12 Sep 2025 09:54:20 +0200 Subject: [PATCH 8/9] Gui: Process minor review comments Co-authored-by: Kacper Donat --- src/Gui/Dialogs/DlgAddProperty.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Gui/Dialogs/DlgAddProperty.cpp b/src/Gui/Dialogs/DlgAddProperty.cpp index c1d80fb90b..1682575e52 100644 --- a/src/Gui/Dialogs/DlgAddProperty.cpp +++ b/src/Gui/Dialogs/DlgAddProperty.cpp @@ -481,14 +481,8 @@ void DlgAddProperty::setAddEnabled(bool enabled) { QPushButton *addButton = ui->buttonBox->button(QDialogButtonBox::Ok); QPushButton *cancelButton = ui->buttonBox->button(QDialogButtonBox::Cancel); - if (enabled) { - cancelButton->setDefault(false); - addButton->setDefault(true); - } - else { - cancelButton->setDefault(true); - addButton->setDefault(false); - } + cancelButton->setDefault(!enabled); + addButton->setDefault(enabled); addButton->setEnabled(enabled); } @@ -550,12 +544,12 @@ bool DlgAddProperty::isTypeValid() bool DlgAddProperty::isDocument() const { - return container && freecad_cast(container); + return container->isDerivedFrom(); } bool DlgAddProperty::isDocumentObject() const { - return container && freecad_cast(container); + return container->isDerivedFrom(); } bool DlgAddProperty::areFieldsValid() @@ -830,9 +824,8 @@ void DlgAddProperty::openTransaction() void DlgAddProperty::critical(const QString& title, const QString& text) { static bool criticalDialogShown = false; if (!criticalDialogShown) { - criticalDialogShown = true; + Base::StateLocker locker(criticalDialogShown); QMessageBox::critical(this, title, text); - criticalDialogShown = false; } } From 1edacbf50818aef9aa3570bde23eae3cb05418ae Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Fri, 12 Sep 2025 10:29:07 +0200 Subject: [PATCH 9/9] Gui: Small refactor of Tree.cpp selecting docs Based on review comment, a small refactoring of code that was duplicated when adding functionality to select documents. The duplication has been removed in this commit. --- src/Gui/Tree.cpp | 44 ++++++++++++++++++++++++-------------------- src/Gui/Tree.h | 1 + 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 93321f06b6..c218cc9435 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -1512,8 +1512,8 @@ void TreeWidget::setupResizableColumn(TreeWidget *tree) { } } -std::vector TreeWidget::getSelectedDocuments() { - std::vector ret; +TreeWidget* TreeWidget::getTreeForSelection() +{ TreeWidget* tree = instance(); if (!tree || !tree->isSelectionAttached()) { for (auto pTree : Instances) @@ -1522,13 +1522,28 @@ std::vector TreeWidget::getSelectedDocuments() { break; } } - if (!tree) - return ret; + if (!tree) { + return nullptr; + } - if (tree->selectTimer->isActive()) + if (tree->selectTimer->isActive()) { tree->onSelectTimer(); - else + } + else { tree->_updateStatus(false); + } + + return tree; +} + +std::vector TreeWidget::getSelectedDocuments() +{ + std::vector ret; + TreeWidget* tree = getTreeForSelection(); + + if (!tree) { + return ret; + } const auto items = tree->selectedItems(); for (auto ti : items) { @@ -1548,22 +1563,11 @@ std::vector TreeWidget::getSelectedDocuments() { std::vector TreeWidget::getSelection(App::Document* doc) { std::vector ret; + TreeWidget* tree = getTreeForSelection(); - TreeWidget* tree = instance(); - if (!tree || !tree->isSelectionAttached()) { - for (auto pTree : Instances) - if (pTree->isSelectionAttached()) { - tree = pTree; - break; - } - } - if (!tree) + if (!tree) { return ret; - - if (tree->selectTimer->isActive()) - tree->onSelectTimer(); - else - tree->_updateStatus(false); + } const auto items = tree->selectedItems(); for (auto ti : items) { diff --git a/src/Gui/Tree.h b/src/Gui/Tree.h index 9555d1254f..dbcf0da88e 100644 --- a/src/Gui/Tree.h +++ b/src/Gui/Tree.h @@ -228,6 +228,7 @@ private: bool CheckForDependents(); void addDependentToSelection(App::Document* doc, App::DocumentObject* docObject); + static TreeWidget* getTreeForSelection(); private: QAction* createGroupAction;