From bfec0992f467be6e66368e416fbc8aa5f79b577a Mon Sep 17 00:00:00 2001 From: Pieter Hijma Date: Tue, 16 Apr 2024 12:19:17 +0200 Subject: [PATCH] Core: Add VarSet support to expression dialog This allows users to directly add properties to variable sets from within the expression input dialog improving a workflow for parameterized design. --- src/Gui/DlgExpressionInput.cpp | 507 +++++++++++++++++++++++++++++++-- src/Gui/DlgExpressionInput.h | 24 ++ src/Gui/DlgExpressionInput.ui | 257 +++++++++++------ 3 files changed, 679 insertions(+), 109 deletions(-) diff --git a/src/Gui/DlgExpressionInput.cpp b/src/Gui/DlgExpressionInput.cpp index a2d8a7cf91..37a08305d2 100644 --- a/src/Gui/DlgExpressionInput.cpp +++ b/src/Gui/DlgExpressionInput.cpp @@ -25,21 +25,30 @@ #include #include #include +#include #endif #include +#include #include #include +#include #include #include "DlgExpressionInput.h" #include "ui_DlgExpressionInput.h" +#include "Application.h" +#include "Command.h" #include "Tools.h" - +#include "ExpressionBinding.h" +#include "BitmapFactory.h" +#include "ViewProviderDocumentObject.h" using namespace App; using namespace Gui::Dialog; +bool DlgExpressionInput::varSetsVisible = false; + DlgExpressionInput::DlgExpressionInput(const App::ObjectIdentifier & _path, std::shared_ptr _expression, const Base::Unit & _impliedUnit, QWidget *parent) @@ -56,6 +65,8 @@ DlgExpressionInput::DlgExpressionInput(const App::ObjectIdentifier & _path, // Setup UI ui->setupUi(this); + initializeVarSets(); + // Connect signal(s) connect(ui->expression, &ExpressionLineEdit::textChanged, this, &DlgExpressionInput::textChanged); @@ -110,6 +121,98 @@ DlgExpressionInput::~DlgExpressionInput() delete ui; } +static void getVarSetsDocument(std::vector& varSets, App::Document* doc) { + for (auto obj : doc->getObjects()) { + auto varSet = dynamic_cast(obj); + if (varSet) { + varSets.push_back(varSet); + } + } +} + +static std::vector getAllVarSets() +{ + std::vector docs = App::GetApplication().getDocuments(); + std::vector varSets; + + for (auto doc : docs) { + getVarSetsDocument(varSets, doc); + } + + return varSets; +} + +Base::Type DlgExpressionInput::getTypePath() +{ + return path.getProperty()->getTypeId(); +} + +Base::Type DlgExpressionInput::determineTypeVarSet() +{ + Base::Type typePath = getTypePath(); + + // The type of the path is leading. If it is one of the types below, we + // can create a property in the varset. + if (typePath == App::PropertyString::getClassTypeId() || + typePath.isDerivedFrom(App::PropertyFloat::getClassTypeId()) || + typePath.isDerivedFrom(App::PropertyInteger::getClassTypeId())) { + return typePath; + } + + // If we cannot determine the type by means of the path, for example when + // dealing with a sketcher constraint list or with the x, y, or z of a + // Placement, the type of the unit allows us to create a property in the + // varset. Since unit properties are derived from App::PropertyFloat, it + // allows us to create a property and set the value. + + std::string unitTypeString = impliedUnit.getTypeString().toStdString(); + if (unitTypeString.empty()) { + // no type was provided + return Base::Type::badType(); + } + + std::string typeString = "App::Property" + unitTypeString; + // may return badType + return Base::Type::fromName(typeString.c_str()); +} + +bool DlgExpressionInput::typeOkForVarSet() +{ + std::string unitType = impliedUnit.getTypeString().toStdString(); + return determineTypeVarSet() != Base::Type::badType(); +} + +void DlgExpressionInput::initializeVarSets() +{ + ui->labelInfoActive->setAlignment(Qt::AlignTop | Qt::AlignLeft); + ui->labelInfoActive->setWordWrap(true); + + connect(ui->checkBoxVarSets, &QCheckBox::stateChanged, + this, &DlgExpressionInput::onCheckVarSets); + connect(ui->comboBoxVarSet, qOverload(&QComboBox::currentIndexChanged), + this, &DlgExpressionInput::onVarSetSelected); + connect(ui->lineEditGroup, &QLineEdit::textChanged, + this, &DlgExpressionInput::onTextChangedGroup); + connect(ui->lineEditPropNew, &QLineEdit::textChanged, + this, &DlgExpressionInput::namePropChanged); + + std::vector varSets = getAllVarSets(); + if (!varSets.empty() && typeOkForVarSet()) { + ui->checkBoxVarSets->setVisible(true); + ui->checkBoxVarSets->setCheckState(varSetsVisible ? Qt::Checked : Qt::Unchecked); + ui->groupBoxVarSets->setVisible(varSetsVisible); + if (varSetsVisible) { + setupVarSets(); + } + } + else { + // The dialog is shown without any VarSet options. + varSetsVisible = false; + ui->checkBoxVarSets->setVisible(false); + ui->groupBoxVarSets->setVisible(false); + } +} + void NumberRange::setRange(double min, double max) { minimum = min; @@ -154,28 +257,8 @@ QPoint DlgExpressionInput::expressionPosition() const return ui->expression->pos(); } -void DlgExpressionInput::textChanged(const QString &text) +void DlgExpressionInput::checkExpression(const QString& text) { - if (text.isEmpty()) { - ui->okBtn->setDisabled(true); - ui->discardBtn->setDefault(true); - return; - } - - ui->okBtn->setDefault(true); - - try { - //resize the input field according to text size - QFontMetrics fm(ui->expression->font()); - int width = QtTools::horizontalAdvance(fm, text) + 15; - if (width < minimumWidth) - ui->expression->setMinimumWidth(minimumWidth); - else - ui->expression->setMinimumWidth(width); - - if(this->width() < ui->expression->minimumWidth()) - setMinimumWidth(ui->expression->minimumWidth()); - //now handle expression std::shared_ptr expr(ExpressionParser::parse(path.getDocumentObject(), text.toUtf8().constData())); @@ -226,6 +309,39 @@ void DlgExpressionInput::textChanged(const QString &text) } } +} + +static const bool NO_CHECK_EXPR = false; + +void DlgExpressionInput::textChanged(const QString &text) +{ + if (text.isEmpty()) { + ui->okBtn->setDisabled(true); + ui->discardBtn->setDefault(true); + return; + } + + ui->okBtn->setDefault(true); + + try { + //resize the input field according to text size + QFontMetrics fm(ui->expression->font()); + int width = QtTools::horizontalAdvance(fm, text) + 15; + if (width < minimumWidth) + ui->expression->setMinimumWidth(minimumWidth); + else + ui->expression->setMinimumWidth(width); + + if(this->width() < ui->expression->minimumWidth()) + setMinimumWidth(ui->expression->minimumWidth()); + + checkExpression(text); + if (varSetsVisible) { + // If varsets are visible, check whether the varset info also + // agrees that the button should be enabled. + // No need to check the expression in that function. + updateVarSetInfo(NO_CHECK_EXPR); + } } catch (Base::Exception & e) { ui->msg->setText(QString::fromUtf8(e.what())); @@ -279,5 +395,352 @@ void DlgExpressionInput::show() ui->expression->selectAll(); } +class Binding : public Gui::ExpressionBinding +{ + // helper class to compensate for the fact that + // ExpressionBinding::setExpression is protected. +public: + Binding() = default; + + void setExpression(std::shared_ptr expr) override + { + ExpressionBinding::setExpression(expr); + } +}; + +static bool isNamePropOk(const QString& nameProp, App::DocumentObject* obj, + std::stringstream& message) +{ + if (!obj) { + message << "Unknown object"; + return false; + } + + std::string name = nameProp.toStdString(); + if (name.empty()) { + message << "Please provide a name for the property."; + return false; + } + + if (name != Base::Tools::getIdentifier(name)) { + message << "Invalid property name (must only contain alphanumericals, underscore, " + << "and must not start with digit"; + return false; + } + + auto prop = obj->getPropertyByName(name.c_str()); + if (prop && prop->getContainer() == obj) { + message << name << " already exists"; + return false; + } + + return true; +} + +static const int ROLE_DOC = Qt::UserRole; +static const int ROLE_VARSET_NAME = Qt::UserRole + 1; +static const int ROLE_VARSET_LABEL = Qt::UserRole + 2; +static const int ROLE_GROUP = Qt::UserRole + 3; + +static QString getValue(QTreeWidgetItem* item, int role) +{ + QVariant variant = item->data(0, role); + return variant.toString(); +} + +void DlgExpressionInput::acceptWithVarSet() +{ + // all checks have been performed in updateVarSetInfo and textChanged that + // decide to enable the button + + // create a property in the VarSet + QTreeWidgetItem *selected = treeWidget->currentItem(); + QString nameVarSet = getValue(selected, ROLE_VARSET_NAME); + QString nameGroup = ui->lineEditGroup->text(); + QString nameProp = ui->lineEditPropNew->text(); + + QString nameDoc = getValue(selected, ROLE_DOC); + App::Document* doc = App::GetApplication().getDocument(nameDoc.toUtf8()); + App::DocumentObject* obj = doc->getObject(nameVarSet.toUtf8()); + + std::string name = nameProp.toStdString(); + std::string group = nameGroup.toStdString(); + std::string type = getType(); + auto prop = obj->addDynamicProperty(type.c_str(), name.c_str(), group.c_str()); + + // Set the value of the property in the VarSet + // + // The value of the property is going to be the value that was originally + // meant to be the value for the property that this dialog is targeting. + Expression* exprSimplfied = expression->simplify(); + auto ne = dynamic_cast(exprSimplfied); + auto se = dynamic_cast(exprSimplfied); + if (ne) { + // the value is a number: directly assign it to the property instead of + // making it an expression in the variable set + Gui::Command::doCommand(Gui::Command::Doc, "App.getDocument('%s').getObject('%s').%s = %f", + obj->getDocument()->getName(), + obj->getNameInDocument(), + prop->getName(), ne->getValue()); + } + else if (se) { + // the value is a string: directly assign it to the property. + Gui::Command::doCommand(Gui::Command::Doc, "App.getDocument('%s').getObject('%s').%s = \"%s\"", + obj->getDocument()->getName(), + obj->getNameInDocument(), + prop->getName(), se->getText().c_str()); + } + else { + // the value is an epxression: make an expression binding in the variable set. + ObjectIdentifier objId(*prop); + Binding binding; + binding.bind(objId); + binding.setExpression(expression); + binding.apply(); + } + + // Create a new expression that refers to the property in the variable set + // for the original property that is the target of this dialog. + expression.reset(ExpressionParser::parse(path.getDocumentObject(), + prop->getFullName().c_str())); +} + +void DlgExpressionInput::accept() { + if (varSetsVisible) { + acceptWithVarSet(); + } + QDialog::accept(); +} + +static void addGroupsVarSetComboBox(App::VarSet* varSet, QTreeWidgetItem* varSetItem) +{ + std::vector properties; + std::set namesGroup; + varSet->getPropertyList(properties); + for (auto prop : properties) { + const char* nameGroup = prop->getGroup(); + if (!nameGroup || strcmp(nameGroup, "") == 0) { + namesGroup.insert("Base"); + } + else { + namesGroup.insert(nameGroup); + } + } + for (const auto& nameGroup : namesGroup) { + // the item will be automatically destroyed when the varSetItem will be destroyed + auto item = new QTreeWidgetItem(varSetItem); + item->setText(0, QString::fromStdString(nameGroup)); + item->setData(0, ROLE_GROUP, QString::fromStdString(nameGroup)); + item->setData(0, ROLE_VARSET_NAME, varSetItem->data(0, ROLE_VARSET_NAME)); + item->setData(0, ROLE_VARSET_LABEL, varSetItem->data(0, ROLE_VARSET_LABEL)); + item->setData(0, ROLE_DOC, varSetItem->data(0, ROLE_DOC)); + } +} + +static void addVarSetsVarSetComboBox(std::vector& varSets, QTreeWidgetItem* docItem) +{ + for (auto varSet : varSets) { + auto vp = Base::freecad_dynamic_cast( + Gui::Application::Instance->getViewProvider(varSet)); + // the item will be automatically destroyed when the docItem will be destroyed + auto item = new QTreeWidgetItem(docItem); + item->setIcon(0, vp->getIcon()); + item->setText(0, QString::fromUtf8(varSet->Label.getValue())); + item->setData(0, ROLE_VARSET_LABEL, QString::fromUtf8(varSet->Label.getValue())); + item->setData(0, ROLE_VARSET_NAME, QString::fromUtf8(varSet->getNameInDocument())); + item->setData(0, ROLE_DOC, docItem->data(0, ROLE_DOC)); + addGroupsVarSetComboBox(varSet, item); + } +} + +static void addDocVarSetComboBox(App::Document* doc, QPixmap& docIcon, QTreeWidgetItem* rootItem) +{ + if (!doc->testStatus(App::Document::TempDoc)) { + std::vector varSets; + getVarSetsDocument(varSets, doc); + if (!varSets.empty()) { + // the item will be automatically destroyed when the rootItem will be destroyed + auto item = new QTreeWidgetItem(rootItem); + item->setIcon(0, docIcon); + item->setText(0, QString::fromUtf8(doc->Label.getValue())); + item->setData(0, ROLE_DOC, QByteArray(doc->getName())); + item->setFlags(Qt::ItemIsEnabled); + item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); + addVarSetsVarSetComboBox(varSets, item); + } + } +} + +static QTreeWidget* createVarSetTreeWidget() +{ + // the caller of the function is responsible of managing the tree widget + auto treeWidget = new QTreeWidget(); + treeWidget->setColumnCount(1); + treeWidget->setHeaderHidden(true); + // the rootItem will be destroyed when the treeWidget will be destroyed + QTreeWidgetItem *rootItem = treeWidget->invisibleRootItem(); + + QPixmap docIcon(Gui::BitmapFactory().pixmap("Document")); + std::vector docs = App::GetApplication().getDocuments(); + + for (auto doc : docs) { + addDocVarSetComboBox(doc, docIcon, rootItem); + } + treeWidget->expandAll(); + + return treeWidget; +} + +void DlgExpressionInput::setupVarSets() +{ + ui->comboBoxVarSet->clear(); + // createVarSetTreeWidget returns a dynamically allocated tree widget + // the memory is managed by means of the unique pointer treeWidget. + treeWidget.reset(createVarSetTreeWidget()); + ui->comboBoxVarSet->setModel(treeWidget->model()); + ui->comboBoxVarSet->setView(treeWidget.get()); + + ui->okBtn->setEnabled(false); +} + +std::string DlgExpressionInput::getType() +{ + return determineTypeVarSet().getName(); +} + +void DlgExpressionInput::onCheckVarSets(int state) { + varSetsVisible = state == Qt::Checked; + ui->groupBoxVarSets->setVisible(varSetsVisible); + if (varSetsVisible) { + setupVarSets(); + } + else { + ui->okBtn->setEnabled(true); // normal expression + } +} + +void DlgExpressionInput::onVarSetSelected(int) +{ + QTreeWidgetItem* selected = treeWidget->currentItem(); + + if (selected) { + // if group is known, fill it in + QVariant variantGroup = selected->data(0, ROLE_GROUP); + if (variantGroup.isValid()) { + ui->lineEditGroup->setText(variantGroup.toString()); + } + else { + ui->lineEditGroup->clear(); + } + } + + updateVarSetInfo(); +} + +void DlgExpressionInput::onTextChangedGroup(const QString&) +{ + updateVarSetInfo(); +} + +void DlgExpressionInput::namePropChanged(const QString&) +{ + updateVarSetInfo(); +} + +static bool isNameGroupOk(const QString& nameGroup, + std::stringstream& message) +{ + std::string name = nameGroup.toStdString(); + if (name.empty() || name != Base::Tools::getIdentifier(name)) { + message << "Invalid group name (must only contain alphanumericals, underscore, " + << "and must not start with digit"; + return false; + } + + return true; +} + +void DlgExpressionInput::reportVarSetInfo(const char* message) +{ + ui->labelInfoActive->setText(QString::fromUtf8(message)); +} + +bool DlgExpressionInput::reportGroup(QString& nameGroup) +{ + if (nameGroup.isEmpty()) { + reportVarSetInfo("Please provide a group."); + return true; + } + + std::stringstream message; + if (!isNameGroupOk(nameGroup, message)) { + reportVarSetInfo(message.str().c_str()); + return true; + } + + return false; +} + +bool DlgExpressionInput::reportName(QTreeWidgetItem* item) +{ + QString nameProp = ui->lineEditPropNew->text(); + QString nameVarSet = getValue(item, ROLE_VARSET_NAME); + QString nameDoc = getValue(item, ROLE_DOC); + App::Document* doc = App::GetApplication().getDocument(nameDoc.toUtf8()); + App::DocumentObject* obj = doc->getObject(nameVarSet.toUtf8()); + std::stringstream message; + if (!isNamePropOk(nameProp, obj, message)) { + reportVarSetInfo(message.str().c_str()); + return true; + } + + return false; +} + +void DlgExpressionInput::updateVarSetInfo(bool checkExpr) +{ + QTreeWidgetItem* selected = treeWidget->currentItem(); + + if (selected) { + QString nameGroup = ui->lineEditGroup->text(); + if (reportGroup(nameGroup)) { + // needed to report something about the group, so disable the button + ui->okBtn->setEnabled(false); + return; + } + + if (reportName(selected)) { + // needed to report something about the name, so disable the button + ui->okBtn->setEnabled(false); + return; + } + + QString nameProp = ui->lineEditPropNew->text(); + QString labelVarSet = getValue(selected, ROLE_VARSET_LABEL); + QString nameDoc = getValue(selected, ROLE_DOC); + std::stringstream message; + message << "Adding property " << nameProp.toStdString() << std::endl + << "of type " << getType() << std::endl + << "to variable set " << labelVarSet.toStdString() << std::endl + << "in group " << nameGroup.toStdString() << std::endl + << "in document " << nameDoc.toStdString() << "."; + + reportVarSetInfo(message.str().c_str()); + if (checkExpr) { + // We have to check the text of the expression as well + try { + checkExpression(ui->expression->text()); + ui->okBtn->setEnabled(true); + } + catch (Base::Exception& e) { + ui->okBtn->setDisabled(true); + } + } + } + else { + ui->okBtn->setEnabled(false); + reportVarSetInfo("Please select a variable set."); + } +} #include "moc_DlgExpressionInput.cpp" diff --git a/src/Gui/DlgExpressionInput.h b/src/Gui/DlgExpressionInput.h index 8bad8ae59f..7cb5c8dd92 100644 --- a/src/Gui/DlgExpressionInput.h +++ b/src/Gui/DlgExpressionInput.h @@ -24,7 +24,9 @@ #define GUI_DIALOG_DLGEXPRESSIONINPUT_H #include +#include #include +#include #include #include @@ -78,14 +80,33 @@ public: public Q_SLOTS: void show(); + void accept() override; protected: void mouseReleaseEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; +private: + Base::Type getTypePath(); + Base::Type determineTypeVarSet(); + bool typeOkForVarSet(); + void initializeVarSets(); + void checkExpression(const QString& text); + void setupVarSets(); + std::string getType(); + void reportVarSetInfo(const char* message); + bool reportName(QTreeWidgetItem* item); + bool reportGroup(QString& nameGroup); + void updateVarSetInfo(bool checkExpr = true); + void acceptWithVarSet(); + private Q_SLOTS: void textChanged(const QString & text); void setDiscarded(); + void onCheckVarSets(int state); + void onVarSetSelected(int); + void onTextChangedGroup(const QString&); + void namePropChanged(const QString&); private: ::Ui::DlgExpressionInput *ui; @@ -96,6 +117,9 @@ private: NumberRange numberRange; int minimumWidth; + + static bool varSetsVisible; + std::unique_ptr treeWidget; }; } diff --git a/src/Gui/DlgExpressionInput.ui b/src/Gui/DlgExpressionInput.ui index e0a56a11ed..3a15631e21 100644 --- a/src/Gui/DlgExpressionInput.ui +++ b/src/Gui/DlgExpressionInput.ui @@ -7,7 +7,7 @@ 0 0 414 - 95 + 272 @@ -29,94 +29,175 @@ 3 - + 0 - - - - - - 0 - - - 0 - - - - - true + + + Variable Sets + + + + + + + + + + + Group: - - QFrame::StyledPanel + + + + + + + 0 + 0 + - - QFrame::Raised + + + 0 + 70 + + + + + + + + + + + Variable Set: + + + + + + + + + + Info: + + + + + + + New Property: + + + + + + + + 0 + 0 + - - - 4 - - - 2 - - - - - Result: - - - - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 190 - 190 - 190 - - - - - - - - - - - - - + + + + + + + + Show variable sets + + + + + + + + + 0 + + + 0 + + + + + true + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 2 + + + + + Result: + + + + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 190 + 190 + 190 + + + + + + + + + + + + + + + @@ -175,12 +256,12 @@ - - &Clear - Revert to last calculated value (as constant) + + &Clear + true @@ -233,10 +314,6 @@ - label - expression - widget - ctrlArea @@ -245,6 +322,12 @@
ExpressionCompleter.h
+ + expression + okBtn + discardBtn + checkBoxVarSets +