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.
This commit is contained in:
Pieter Hijma
2024-04-16 12:19:17 +02:00
committed by Chris Hennes
parent f331c91b89
commit bfec0992f4
3 changed files with 679 additions and 109 deletions

View File

@@ -25,21 +25,30 @@
#include <QApplication>
#include <QMenu>
#include <QMouseEvent>
#include <QTreeWidget>
#endif
#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/ExpressionParser.h>
#include <App/VarSet.h>
#include <Base/Tools.h>
#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<const Expression> _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<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().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<int>(&QComboBox::currentIndexChanged),
this, &DlgExpressionInput::onVarSetSelected);
connect(ui->lineEditGroup, &QLineEdit::textChanged,
this, &DlgExpressionInput::onTextChangedGroup);
connect(ui->lineEditPropNew, &QLineEdit::textChanged,
this, &DlgExpressionInput::namePropChanged);
std::vector<App::VarSet*> 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<Expression> 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<App::Expression> 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<NumberExpression*>(exprSimplfied);
auto se = dynamic_cast<StringExpression*>(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<Property*> properties;
std::set<std::string> 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<App::VarSet*>& varSets, QTreeWidgetItem* docItem)
{
for (auto varSet : varSets) {
auto vp = Base::freecad_dynamic_cast<Gui::ViewProviderDocumentObject>(
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<App::VarSet*> 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<App::Document*> 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"