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