Download src/Gui/Dialogs/DlgExpressionInput.cpp from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 34.2 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Gui/Dialogs/DlgExpressionInput.cpp
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Gui/Dialogs/DlgExpressionInput.cpp
-
curl -L -o DlgExpressionInput.cpp https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Gui/Dialogs/DlgExpressionInput.cpp
34.2 kB
| // SPDX-License-Identifier: LGPL-2.1-or-later | |
| /*************************************************************************** | |
| * Copyright (c) 2015 Eivind Kvedalen <eivind@kvedalen.name> * | |
| * Copyright (c) 2025 Pieter Hijma <info@pieterhijma.net> * | |
| * * | |
| * 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 * | |
| * * | |
| ***************************************************************************/ | |
| using namespace App; | |
| using namespace Gui::Dialog; | |
| FC_LOG_LEVEL_INIT("DlgExpressionInput", true, true) | |
| DlgExpressionInput::DlgExpressionInput( | |
| const App::ObjectIdentifier& _path, | |
| std::shared_ptr<const Expression> _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<QString>()) { | |
| 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) { | |
| setWindowFlags(Qt::Widget | Qt::Popup | Qt::FramelessWindowHint); | |
| setWindowFlags(Qt::SubWindow | Qt::Widget | Qt::Popup | Qt::FramelessWindowHint); | |
| 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() | |
| { | |
| disconnect( | |
| ui->checkBoxVarSets, | |
| &QCheckBox::checkStateChanged, | |
| this, | |
| &DlgExpressionInput::onCheckVarSets | |
| ); | |
| disconnect(ui->checkBoxVarSets, &QCheckBox::stateChanged, this, &DlgExpressionInput::onCheckVarSets); | |
| disconnect( | |
| ui->comboBoxVarSet, | |
| qOverload<int>(&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<App::VarSet*>& varSets, App::Document* doc) | |
| { | |
| for (auto obj : doc->getObjects()) { | |
| auto varSet = dynamic_cast<App::VarSet*>(obj); | |
| if (varSet) { | |
| varSets.push_back(varSet); | |
| } | |
| } | |
| } | |
| static std::vector<App::VarSet*> getAllVarSets() | |
| { | |
| std::vector<App::Document*> docs = App::GetApplication().getDocuments(); | |
| std::vector<App::VarSet*> 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() | |
| { | |
| connect(ui->checkBoxVarSets, &QCheckBox::checkStateChanged, this, &DlgExpressionInput::onCheckVarSets); | |
| connect(ui->checkBoxVarSets, &QCheckBox::stateChanged, this, &DlgExpressionInput::onCheckVarSets); | |
| connect( | |
| ui->comboBoxVarSet, | |
| qOverload<int>(&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<App::VarSet*> 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<Expression> 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<Expression> 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<Expression> 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<NumberExpression*>(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<App::Expression> 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<const App::NumberExpression*>(expr); | |
| } | |
| static const App::StringExpression* toStringExpr(const App::Expression* expr) | |
| { | |
| return freecad_cast<const App::StringExpression*>(expr); | |
| } | |
| static const App::OperatorExpression* toUnitNumberExpr(const App::Expression* expr) | |
| { | |
| auto* opExpr = freecad_cast<const App::OperatorExpression*>(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<App::ObjectIdentifier, bool> identifiers = expression->getIdentifiers(); | |
| std::map<ObjectIdentifier, ObjectIdentifier> 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<QStandardItemModel*>(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<App::VarSet*>& varSets, | |
| QStandardItem* docItem, | |
| QStandardItemModel* model | |
| ) | |
| { | |
| for (auto* varSet : varSets) { | |
| auto* vp = freecad_cast<Gui::ViewProviderDocumentObject*>( | |
| 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<App::VarSet*> 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<App::Document*> 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<int>(wrappedMsg.size()) | |
| > msgContentWidth / msgFontMetrics.averageCharWidth() * msgLinesLimit) { | |
| const QString elidedMsg = msgFontMetrics.elidedText( | |
| QString::fromStdString(wrappedMsg), | |
| Qt::ElideRight, | |
| msgContentWidth * msgLinesLimit | |
| ); | |
| ui->msg->setText(elidedMsg); | |
| } | |
| } | |