// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2015 Eivind Kvedalen * * 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., 51 Franklin Street, * * Fifth Floor, Boston, MA 02110-1301, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Dialogs/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; FC_LOG_LEVEL_INIT("DlgExpressionInput", true, true) DlgExpressionInput::DlgExpressionInput( const App::ObjectIdentifier& _path, std::shared_ptr _expression, const Base::Unit& _impliedUnit, QWidget* parent ) : QDialog(parent) , ui(new Ui::DlgExpressionInput) , expression(_expression ? _expression->copy() : nullptr) , path(_path) , discarded(false) , impliedUnit(_impliedUnit) , varSetsVisible(false) , comboBoxGroup(this) { assert(path.getDocumentObject()); // Setup UI ui->setupUi(this); okBtn = ui->buttonBox->button(QDialogButtonBox::Ok); discardBtn = ui->buttonBox->button(QDialogButtonBox::Reset); discardBtn->setToolTip(tr("Revert to last calculated value (as constant)")); initializeVarSets(); // Connect signal(s) connect(ui->expression, &ExpressionTextEdit::textChanged, this, &DlgExpressionInput::textChanged); connect(discardBtn, &QPushButton::clicked, this, &DlgExpressionInput::setDiscarded); if (expression) { ui->expression->setPlainText(QString::fromStdString(expression->toString())); } else { QVariant text = parent->property("text"); if (text.canConvert()) { ui->expression->setPlainText(text.toString()); } } // Set document object on text edit to create auto completer DocumentObject* docObj = path.getDocumentObject(); ui->expression->setDocumentObject(docObj); // There are some platforms where setting no system background causes a black // rectangle to appear. To avoid this the 'NoSystemBackground' parameter can be // set to false. Then a normal non-modal dialog will be shown instead (#0002440). bool noBackground = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Expression") ->GetBool("NoSystemBackground", false); if (noBackground) { #if defined(Q_OS_MACOS) setWindowFlags(Qt::Widget | Qt::Popup | Qt::FramelessWindowHint); #else setWindowFlags(Qt::SubWindow | Qt::Widget | Qt::Popup | Qt::FramelessWindowHint); #endif setAttribute(Qt::WA_NoSystemBackground, true); setAttribute(Qt::WA_TranslucentBackground, true); } else { ui->expression->setMinimumWidth(300); ui->expression->setMinimumHeight(80); ui->msg->setWordWrap(true); ui->msg->setMaximumHeight(200); ui->msg->setMinimumWidth(280); ui->verticalLayout->setContentsMargins(9, 9, 9, 9); this->adjustSize(); // It is strange that (at least on Linux) DlgExpressionInput will shrink // to be narrower than ui->expression after calling adjustSize() above. // Why? if (this->width() < ui->expression->width() + 18) { this->resize(ui->expression->width() + 18, this->height()); } } ui->expression->setFocus(); } DlgExpressionInput::~DlgExpressionInput() { #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) disconnect( ui->checkBoxVarSets, &QCheckBox::checkStateChanged, this, &DlgExpressionInput::onCheckVarSets ); #else disconnect(ui->checkBoxVarSets, &QCheckBox::stateChanged, this, &DlgExpressionInput::onCheckVarSets); #endif disconnect( ui->comboBoxVarSet, qOverload(&QComboBox::currentIndexChanged), this, &DlgExpressionInput::onVarSetSelected ); disconnect( &comboBoxGroup, &EditFinishedComboBox::currentTextChanged, this, &DlgExpressionInput::onTextChangedGroup ); disconnect(ui->lineEditPropNew, &QLineEdit::textChanged, this, &DlgExpressionInput::namePropChanged); 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(); 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(); return !determineTypeVarSet().isBad(); } void DlgExpressionInput::initializeErrorFrame() { ui->errorFrame->setVisible(false); const int size = style()->pixelMetric(QStyle::PM_LargeIconSize); QIcon icon = Gui::BitmapFactory().iconFromTheme("overlay_error"); if (icon.isNull()) { icon = style()->standardIcon(QStyle::SP_MessageBoxCritical); } ui->errorIconLabel->setPixmap(icon.pixmap(QSize(size, size))); } void DlgExpressionInput::initializeVarSets() { #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) connect(ui->checkBoxVarSets, &QCheckBox::checkStateChanged, this, &DlgExpressionInput::onCheckVarSets); #else connect(ui->checkBoxVarSets, &QCheckBox::stateChanged, this, &DlgExpressionInput::onCheckVarSets); #endif connect( ui->comboBoxVarSet, qOverload(&QComboBox::currentIndexChanged), this, &DlgExpressionInput::onVarSetSelected ); connect( &comboBoxGroup, &EditFinishedComboBox::currentTextChanged, this, &DlgExpressionInput::onTextChangedGroup ); connect(ui->lineEditPropNew, &QLineEdit::textChanged, this, &DlgExpressionInput::namePropChanged); comboBoxGroup.setObjectName(QStringLiteral("comboBoxGroup")); comboBoxGroup.setInsertPolicy(QComboBox::InsertAtTop); comboBoxGroup.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); DlgAddProperty::setWidgetForLabel("labelGroup", &comboBoxGroup, ui->formLayout); setTabOrder(ui->comboBoxVarSet, &comboBoxGroup); setTabOrder(&comboBoxGroup, ui->lineEditPropNew); std::vector varSets = getAllVarSets(); if (!varSets.empty() && typeOkForVarSet()) { ui->checkBoxVarSets->setVisible(true); ui->checkBoxVarSets->setCheckState(Qt::Unchecked); ui->groupBoxVarSets->setVisible(false); } else { // The dialog is shown without any VarSet options. ui->checkBoxVarSets->setVisible(false); ui->groupBoxVarSets->setVisible(false); } initializeErrorFrame(); } void NumberRange::setRange(double min, double max) { minimum = min; maximum = max; defined = true; } void NumberRange::clearRange() { defined = false; } void NumberRange::throwIfOutOfRange(const Base::Quantity& value) const { if (!defined) { return; } auto toQString = [](const Base::Quantity& v) { return QString::fromStdString(v.getUserString()); }; if (value.getValue() < minimum || value.getValue() > maximum) { Base::Quantity minVal(minimum, value.getUnit()); Base::Quantity maxVal(maximum, value.getUnit()); const QString fmt = QCoreApplication::translate("Exceptions", "Value out of range (%1 out of [%2, %3])"); const QString msg = fmt.arg(toQString(value), toQString(minVal), toQString(maxVal)); THROWM(Base::ValueError, msg.toStdString()); } } void DlgExpressionInput::setRange(double minimum, double maximum) { numberRange.setRange(minimum, maximum); } void DlgExpressionInput::clearRange() { numberRange.clearRange(); } QPoint DlgExpressionInput::expressionPosition() const { return ui->expression->pos(); } bool DlgExpressionInput::checkCyclicDependencyVarSet(const QString& text) { std::shared_ptr expr( ExpressionParser::parse(path.getDocumentObject(), text.toUtf8().constData()) ); if (expr) { DocumentObject* obj = path.getDocumentObject(); auto ids = expr->getIdentifiers(); for (const auto& id : ids) { if (id.first.getDocumentObject() == obj) { // This string is not translated. It is based on a string that // originates from the expression validator in App that is also // not translated. ui->msg->setText( QString::fromStdString(id.first.toString() + " reference causes a cyclic dependency") ); return true; } } } return false; } void DlgExpressionInput::checkExpression(const QString& text) { // now handle expression std::shared_ptr expr( ExpressionParser::parse(path.getDocumentObject(), text.toUtf8().constData()) ); if (expr) { std::string error = path.getDocumentObject()->ExpressionEngine.validateExpression(path, expr); if (!error.empty()) { throw Base::RuntimeError(error.c_str()); } std::unique_ptr result(expr->eval()); expression = expr; okBtn->setEnabled(true); ui->msg->clear(); // set default palette as we may have read text right now ui->msg->setPalette(okBtn->palette()); auto* n = freecad_cast(result.get()); if (n) { Base::Quantity value = n->getQuantity(); if (!value.isValid()) { THROWMT(Base::ValueError, QT_TRANSLATE_NOOP("Exceptions", "Not a number")); } QString msg = QString::fromStdString(value.getUserString()); if (impliedUnit != Base::Unit::One) { if (!value.isDimensionless() && value.getUnit() != impliedUnit) { THROWMT( Base::UnitsMismatchError, QT_TRANSLATE_NOOP("Exceptions", "Unit mismatch between result and required unit") ); } value.setUnit(impliedUnit); } else if (!value.isDimensionless()) { msg += tr(" (Warning: unit discarded)"); QPalette p(ui->msg->palette()); p.setColor(QPalette::WindowText, Qt::red); ui->msg->setPalette(p); } numberRange.throwIfOutOfRange(value); message = msg.toStdString(); } else { message = result->toString(); } setMsgText(); } } static const bool NoCheckExpr = false; void DlgExpressionInput::textChanged() { const QString& text = ui->expression->toPlainText(); if (text.isEmpty()) { okBtn->setDisabled(true); discardBtn->setDefault(true); return; } okBtn->setDefault(true); try { 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(NoCheckExpr); } } catch (Base::Exception& e) { message = e.what(); setMsgText(); QPalette p(ui->msg->palette()); p.setColor(QPalette::WindowText, Qt::red); ui->msg->setPalette(p); okBtn->setDisabled(true); } } void DlgExpressionInput::setDiscarded() { discarded = true; reject(); } void DlgExpressionInput::mouseReleaseEvent(QMouseEvent* event) { Q_UNUSED(event); } void DlgExpressionInput::mousePressEvent(QMouseEvent* event) { Q_UNUSED(event); // The 'FramelessWindowHint' is also set when the background is transparent. if (windowFlags() & Qt::FramelessWindowHint) { // we need to reject the dialog when clicked on the background. As the background is // transparent this is the expected behaviour for the user bool on = ui->expression->completerActive(); if (!on) { this->reject(); } } } void DlgExpressionInput::show() { QDialog::show(); this->activateWindow(); 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 constexpr const char* InvalidIdentifierMessage = QT_TR_NOOP( "must contain only alphanumeric characters, underscore, and must not start with a digit" ); bool DlgExpressionInput::isPropertyNameValid( const QString& nameProp, const App::DocumentObject* obj, QString& message ) const { auto withPrefix = [&](const QString& detail) { return tr("Invalid property name: %1").arg(detail); }; if (!obj) { message = tr("Unknown object"); return false; } std::string name = nameProp.toStdString(); if (name.empty()) { message = withPrefix(tr("the name cannot be empty")); return false; } if (name != Base::Tools::getIdentifier(name)) { message = withPrefix(tr(InvalidIdentifierMessage)); return false; } if (ExpressionParser::isTokenAUnit(name)) { message = withPrefix(tr("%1 is a unit").arg(nameProp)); return false; } if (ExpressionParser::isTokenAConstant(name)) { message = withPrefix(tr("%1 is a constant").arg(nameProp)); return false; } auto prop = obj->getPropertyByName(name.c_str()); if (prop && prop->getContainer() == obj) { message = withPrefix(tr("%1 already exists").arg(nameProp)); return false; } return true; } static const int DocRole = Qt::UserRole; static const int VarSetNameRole = Qt::UserRole + 1; static const int VarSetLabelRole = Qt::UserRole + 2; static const int LevelRole = Qt::UserRole + 3; static QString getValue(QComboBox* comboBox, int role) { QVariant variant = comboBox->currentData(role); return variant.toString(); } static void storePreferences( const std::string& nameDoc, const std::string& nameVarSet, const std::string& nameGroup ) { auto paramExpressionEditor = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/ExpressionEditor" ); paramExpressionEditor->SetASCII("LastDocument", nameDoc); paramExpressionEditor->SetASCII("LastVarSet", nameVarSet); paramExpressionEditor->SetASCII("LastGroup", nameGroup); } static const App::NumberExpression* toNumberExpr(const App::Expression* expr) { return freecad_cast(expr); } static const App::StringExpression* toStringExpr(const App::Expression* expr) { return freecad_cast(expr); } static const App::OperatorExpression* toUnitNumberExpr(const App::Expression* expr) { auto* opExpr = freecad_cast(expr); if (opExpr && opExpr->getOperator() == App::OperatorExpression::Operator::UNIT && toNumberExpr(opExpr->getLeft())) { return opExpr; } return nullptr; } void DlgExpressionInput::createBindingVarSet(App::Property* propVarSet, App::DocumentObject* varSet) { ObjectIdentifier varSetId(*propVarSet); // rewrite the identifiers of the expression to be relative to the VarSet std::map identifiers = expression->getIdentifiers(); std::map idsFromObjToVarSet; for (const auto& idPair : identifiers) { ObjectIdentifier exprId = idPair.first; ObjectIdentifier relativeId = exprId.relativeTo(varSetId); idsFromObjToVarSet[exprId] = relativeId; } Binding binding; binding.bind(*propVarSet); binding.setExpression(expression); binding.apply(); varSet->renameObjectIdentifiers(idsFromObjToVarSet); } void DlgExpressionInput::acceptWithVarSet() { // all checks have been performed in updateVarSetInfo and textChanged that // decide to enable the button // create a property in the VarSet QString nameVarSet = getValue(ui->comboBoxVarSet, VarSetNameRole); QString nameGroup = comboBoxGroup.currentText(); QString nameProp = ui->lineEditPropNew->text(); QString nameDoc = getValue(ui->comboBoxVarSet, DocRole); 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. const Expression* expr = expression.get(); if (const NumberExpression* ne = toNumberExpr(expr)) { // 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 (const StringExpression* se = toStringExpr(expr)) { // 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 if (const OperatorExpression* une = toUnitNumberExpr(expr)) { // the value is a unit number: 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(), une->toString().c_str() ); } else { // the value is an expression: make an expression binding in the VarSet createBindingVarSet(prop, obj); } // Create a new expression that refers to the property in the VarSet // for the original property that is the target of this dialog. expression.reset(ExpressionParser::parse(path.getDocumentObject(), prop->getFullName().c_str())); storePreferences(nameDoc.toStdString(), nameVarSet.toStdString(), group); } void DlgExpressionInput::accept() { if (varSetsVisible) { if (needReportOnVarSet()) { return; } acceptWithVarSet(); } QDialog::accept(); } static App::Document* getPreselectedDocument() { auto paramExpressionEditor = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/ExpressionEditor" ); std::string lastDoc = paramExpressionEditor->GetASCII("LastDocument", ""); if (lastDoc.empty()) { return App::GetApplication().getActiveDocument(); } App::Document* doc = App::GetApplication().getDocument(lastDoc.c_str()); if (doc == nullptr) { return App::GetApplication().getActiveDocument(); } return doc; } int DlgExpressionInput::getVarSetIndex(const App::Document* doc) const { auto paramExpressionEditor = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/ExpressionEditor" ); std::string lastVarSet = paramExpressionEditor->GetASCII("LastVarSet", "VarSet"); auto* model = qobject_cast(ui->comboBoxVarSet->model()); for (int i = 0; i < model->rowCount(); ++i) { QStandardItem* item = model->item(i); if (item->data(DocRole).toString() == QString::fromUtf8(doc->getName()) && item->data(VarSetNameRole).toString() == QString::fromStdString(lastVarSet)) { return i; } } // Select the first varset of the first document (the document is item 0) return 1; } void DlgExpressionInput::preselectVarSet() { const App::Document* doc = getPreselectedDocument(); if (doc == nullptr) { FC_ERR("No active document found"); } ui->comboBoxVarSet->setCurrentIndex(getVarSetIndex(doc)); } // Custom delegate to add indentation class IndentedItemDelegate: public QStyledItemDelegate { public: explicit IndentedItemDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override { QStyledItemDelegate::initStyleOption(option, index); if (index.data(LevelRole) == 1) { int indentWidth = 20; option->rect.adjust(indentWidth, 0, 0, 0); } } }; static void addVarSetsVarSetComboBox( std::vector& varSets, QStandardItem* docItem, QStandardItemModel* model ) { for (auto* varSet : varSets) { auto* vp = freecad_cast( Gui::Application::Instance->getViewProvider(varSet) ); if (vp == nullptr) { FC_ERR("No ViewProvider found for VarSet: " << varSet->getNameInDocument()); continue; } // The item will be owned by the model, so no need to delete it manually. auto item = new QStandardItem(); item->setIcon(vp->getIcon()); item->setText(QString::fromUtf8(varSet->Label.getValue())); item->setData(QString::fromUtf8(varSet->Label.getValue()), VarSetLabelRole); item->setData(QString::fromUtf8(varSet->getNameInDocument()), VarSetNameRole); item->setData(docItem->data(DocRole), DocRole); item->setData(1, LevelRole); model->appendRow(item); } } static void addDocVarSetComboBox(App::Document* doc, QPixmap& docIcon, QStandardItemModel* model) { if (doc->testStatus(App::Document::TempDoc)) { // Do not add temporary documents to the VarSet combo box return; } std::vector varSets; getVarSetsDocument(varSets, doc); if (varSets.empty()) { return; } // The item will be owned by the model, so no need to delete it manually. auto* item = new QStandardItem(); item->setIcon(docIcon); item->setText(QString::fromUtf8(doc->Label.getValue())); item->setData(QByteArray(doc->getName()), DocRole); item->setFlags(Qt::ItemIsEnabled); // Make sure this item cannot be selected item->setData(0, LevelRole); model->appendRow(item); addVarSetsVarSetComboBox(varSets, item, model); } QStandardItemModel* DlgExpressionInput::createVarSetModel() { // Create the model auto* model = new QStandardItemModel(ui->comboBoxVarSet); model->setColumnCount(1); // Add items to the model QPixmap docIcon(Gui::BitmapFactory().pixmap("Document")); std::vector docs = App::GetApplication().getDocuments(); for (auto doc : docs) { addDocVarSetComboBox(doc, docIcon, model); } return model; } void DlgExpressionInput::setupVarSets() { QStandardItemModel* model = createVarSetModel(); { QSignalBlocker blocker(ui->comboBoxVarSet); ui->comboBoxVarSet->clear(); auto* listView = new QListView(this); listView->setSelectionMode(QAbstractItemView::SingleSelection); listView->setModel(model); ui->comboBoxVarSet->setView(listView); ui->comboBoxVarSet->setModel(model); ui->comboBoxVarSet->setItemDelegate(new IndentedItemDelegate(ui->comboBoxVarSet)); } preselectVarSet(); 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 { try { checkExpression(ui->expression->toPlainText()); } catch (Base::Exception&) { okBtn->setEnabled(false); } adjustSize(); } } void DlgExpressionInput::preselectGroup() { auto paramExpressionEditor = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/ExpressionEditor" ); std::string lastGroup = paramExpressionEditor->GetASCII("LastGroup", ""); if (lastGroup.empty()) { return; } if (int index = comboBoxGroup.findText(QString::fromStdString(lastGroup)); index != -1) { comboBoxGroup.setCurrentIndex(index); } } void DlgExpressionInput::onVarSetSelected(int /*index*/) { QString docName = getValue(ui->comboBoxVarSet, DocRole); QString varSetName = getValue(ui->comboBoxVarSet, VarSetNameRole); if (docName.isEmpty() || varSetName.isEmpty()) { FC_ERR("No document or variable set selected"); return; } App::Document* doc = App::GetApplication().getDocument(docName.toUtf8()); if (doc == nullptr) { FC_ERR("Document not found: " << docName.toStdString()); return; } App::DocumentObject* varSet = doc->getObject(varSetName.toUtf8()); if (varSet == nullptr) { FC_ERR("Variable set not found: " << varSetName.toStdString()); return; } DlgAddProperty::populateGroup(comboBoxGroup, varSet); preselectGroup(); updateVarSetInfo(); ui->lineEditPropNew->setFocus(); } void DlgExpressionInput::onTextChangedGroup(const QString&) { updateVarSetInfo(); } void DlgExpressionInput::namePropChanged(const QString&) { updateVarSetInfo(); } bool DlgExpressionInput::isGroupNameValid(const QString& nameGroup, QString& message) const { auto withPrefix = [&](const QString& detail) { return tr("Invalid group name: %1").arg(detail); }; if (nameGroup.isEmpty()) { message = withPrefix(tr("the name cannot be empty")); return false; } std::string name = nameGroup.toStdString(); if (name != Base::Tools::getIdentifier(name)) { message = withPrefix(tr(InvalidIdentifierMessage)); return false; } return true; } void DlgExpressionInput::reportVarSetInfo(const QString& message) { if (!message.isEmpty()) { ui->errorFrame->setVisible(true); ui->errorTextLabel->setText(message); ui->errorTextLabel->updateGeometry(); } } static void setErrorState(QWidget* widget, bool on) { widget->setProperty("validationState", on ? QStringLiteral("error") : QVariant()); widget->style()->unpolish(widget); widget->style()->polish(widget); } bool DlgExpressionInput::reportGroup(const QString& nameGroup) { QString message; if (!isGroupNameValid(nameGroup, message)) { setErrorState(&comboBoxGroup, true); reportVarSetInfo(message); return true; } return false; } bool DlgExpressionInput::reportName() { QString nameProp = ui->lineEditPropNew->text(); QString nameVarSet = getValue(ui->comboBoxVarSet, VarSetNameRole); QString nameDoc = getValue(ui->comboBoxVarSet, DocRole); App::Document* doc = App::GetApplication().getDocument(nameDoc.toUtf8()); App::DocumentObject* obj = doc->getObject(nameVarSet.toUtf8()); QString message; if (!isPropertyNameValid(nameProp, obj, message)) { setErrorState(ui->lineEditPropNew, true); reportVarSetInfo(message); return true; } return false; } void DlgExpressionInput::updateVarSetInfo(bool checkExpr) { if (ui->lineEditPropNew->text().isEmpty()) { okBtn->setEnabled(false); return; } if (comboBoxGroup.currentText().isEmpty()) { okBtn->setEnabled(false); return; } if (checkCyclicDependencyVarSet(ui->expression->toPlainText())) { okBtn->setEnabled(false); return; } if (checkExpr) { // We have to check the text of the expression as well try { checkExpression(ui->expression->toPlainText()); } catch (Base::Exception&) { okBtn->setEnabled(false); } } okBtn->setEnabled(true); } bool DlgExpressionInput::needReportOnVarSet() { setErrorState(ui->lineEditPropNew, false); setErrorState(&comboBoxGroup, false); return reportGroup(comboBoxGroup.currentText()) || reportName(); } void DlgExpressionInput::resizeEvent(QResizeEvent* event) { // When the dialog is resized, message text may need to be re-wrapped if (!this->message.empty() && event->size() != event->oldSize()) { setMsgText(); } QDialog::resizeEvent(event); } void DlgExpressionInput::setMsgText() { if (!this->message.size()) { return; } const QFontMetrics msgFontMetrics {ui->msg->font()}; // find words longer than length of msg widget // then insert newline to wrap it std::string wrappedMsg {}; const int msgContentWidth = ui->msg->width() * 0.85; // 0.85 is a magic number for some padding const int maxWordLength = msgContentWidth / msgFontMetrics.averageCharWidth(); const auto wrappableWordPattern = std::regex {"\\S{" + std::to_string(maxWordLength) + "}"}; auto it = std::sregex_iterator {this->message.cbegin(), this->message.cend(), wrappableWordPattern}; const auto itEnd = std::sregex_iterator {}; int lastPos = 0; for (; it != itEnd; ++it) { wrappedMsg += this->message.substr(lastPos, it->position() - lastPos); wrappedMsg += it->str() + "\n"; lastPos = it->position() + it->length(); } wrappedMsg += this->message.substr(lastPos); ui->msg->setText(QString::fromStdString(wrappedMsg)); // elide text if it is going out of widget bounds // note: this is only 'rough elide', as this text is usually not very long; const int msgLinesLimit = 3; if (static_cast(wrappedMsg.size()) > msgContentWidth / msgFontMetrics.averageCharWidth() * msgLinesLimit) { const QString elidedMsg = msgFontMetrics.elidedText( QString::fromStdString(wrappedMsg), Qt::ElideRight, msgContentWidth * msgLinesLimit ); ui->msg->setText(elidedMsg); } } #include "moc_DlgExpressionInput.cpp"