Files
create/src/Gui/DlgAddPropertyVarSet.cpp
Pieter Hijma fc6120b8dd Gui: Prevent invalid editors in VarSet dialog
In the VarSet dialog, we can create an editor after the name and type
has been determined.  However, if the name is changed after an editor
has been created, the editor is invalid because the underlying property
has been removed.  In that case, the function onNameDetermined() should
clean up the invalid editor and this happens in most cases.
Unfortunately, it cannot handle the case in which a click happens on the
invalid editor itself.  This click should result in onNameDetermined() but
since the editor is already invalid, onNameDetermined() is triggered too
late.

The current commit solves this by listening for every change in the name
of the property and handle the editors accordingly.
2024-07-13 22:51:46 -05:00

408 lines
15 KiB
C++

/****************************************************************************
* Copyright (c) 2024 Ondsel <development@ondsel.com> *
* *
* 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., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
****************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
# include <QMessageBox>
# include <QString>
# include <QCompleter>
#endif
#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/PropertyUnits.h>
#include <Base/Tools.h>
#include "DlgAddPropertyVarSet.h"
#include "ui_DlgAddPropertyVarSet.h"
#include "MainWindow.h"
#include "ViewProviderDocumentObject.h"
#include "ViewProviderVarSet.h"
FC_LOG_LEVEL_INIT("DlgAddPropertyVarSet", true, true)
using namespace Gui;
using namespace Gui::Dialog;
const std::string DlgAddPropertyVarSet::GROUP_BASE = "Base";
DlgAddPropertyVarSet::DlgAddPropertyVarSet(QWidget* parent,
ViewProviderVarSet* viewProvider)
: QDialog(parent),
varSet(dynamic_cast<App::VarSet*>(viewProvider->getObject())),
ui(new Ui_DlgAddPropertyVarSet),
comboBoxGroup(this),
completerType(this),
editor(nullptr)
{
ui->setupUi(this);
initializeWidgets(viewProvider);
}
DlgAddPropertyVarSet::~DlgAddPropertyVarSet() = default;
void DlgAddPropertyVarSet::initializeGroup()
{
connect(&comboBoxGroup, &EditFinishedComboBox::editFinished,
this, &DlgAddPropertyVarSet::onGroupDetermined);
comboBoxGroup.setObjectName(QString::fromUtf8("comboBoxGroup"));
comboBoxGroup.setInsertPolicy(QComboBox::InsertAtTop);
comboBoxGroup.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
comboBoxGroup.setEditable(true);
auto formLayout = qobject_cast<QFormLayout*>(layout());
formLayout->setWidget(1, QFormLayout::FieldRole, &comboBoxGroup);
std::vector<App::Property*> properties;
varSet->getPropertyList(properties);
std::unordered_set<std::string> groupNames;
for (auto prop : properties) {
const char* groupName = varSet->getPropertyGroup(prop);
groupNames.insert(groupName ? groupName : GROUP_BASE);
}
std::vector<std::string> groupNamesSorted(groupNames.begin(), groupNames.end());
std::sort(groupNamesSorted.begin(), groupNamesSorted.end(), [](std::string& a, std::string& b) {
// prefer anything else other than Base, so move it to the back
if (a == GROUP_BASE) {
return false;
}
else if (b == GROUP_BASE) {
return true;
}
else {
return a < b;
}
});
for (const auto& groupName : groupNamesSorted) {
comboBoxGroup.addItem(QString::fromStdString(groupName));
}
comboBoxGroup.setEditText(QString::fromStdString(groupNamesSorted[0]));
}
void DlgAddPropertyVarSet::initializeTypes()
{
auto paramGroup = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/PropertyView");
auto lastType = Base::Type::fromName(
paramGroup->GetASCII("NewPropertyType","App::PropertyLength").c_str());
if(lastType.isBad()) {
lastType = App::PropertyLength::getClassTypeId();
}
std::vector<Base::Type> types;
Base::Type::getAllDerivedFrom(Base::Type::fromName("App::Property"),types);
std::sort(types.begin(), types.end(), [](Base::Type a, Base::Type b) { return strcmp(a.getName(), b.getName()) < 0; });
for(const auto& type : types) {
ui->comboBoxType->addItem(QString::fromLatin1(type.getName()));
if(type == lastType)
ui->comboBoxType->setCurrentIndex(ui->comboBoxType->count()-1);
}
completerType.setModel(ui->comboBoxType->model());
completerType.setCaseSensitivity(Qt::CaseInsensitive);
completerType.setFilterMode(Qt::MatchContains);
ui->comboBoxType->setCompleter(&completerType);
ui->comboBoxType->setInsertPolicy(QComboBox::NoInsert);
connect(ui->comboBoxType, qOverload<int>(&QComboBox::currentIndexChanged),
this, &DlgAddPropertyVarSet::onTypePropertyDetermined);
}
/*
// keep some debugging code for debugging tab order
static void printFocusChain(QWidget *widget) {
FC_ERR("Focus Chain:");
QWidget* start = widget;
int i = 0;
do {
FC_ERR(" " << widget->objectName().toUtf8().constData());
widget = widget->nextInFocusChain();
i++;
} while (widget != nullptr && i < 30 && start != widget);
QWidget *currentWidget = QApplication::focusWidget();
FC_ERR(" Current focus widget:" << (currentWidget ? currentWidget->objectName().toUtf8().constData() : "None") << std::endl << std::endl);
}
*/
void DlgAddPropertyVarSet::initializeWidgets(ViewProviderVarSet* viewProvider)
{
initializeGroup();
initializeTypes();
connect(this, &QDialog::finished,
this, [viewProvider](int result) { viewProvider->onFinished(result); });
connect(ui->lineEditName, &QLineEdit::textChanged,
this, &DlgAddPropertyVarSet::onNamePropertyDetermined);
std::string title = "Add a property to " + varSet->getFullName();
setWindowTitle(QString::fromStdString(title));
setOkEnabled(false);
ui->lineEditName->setFocus();
QWidget::setTabOrder(ui->lineEditName, &comboBoxGroup);
QWidget::setTabOrder(&comboBoxGroup, ui->comboBoxType);
// FC_ERR("Initialize widgets");
// printFocusChain(ui->lineEditName);
}
void DlgAddPropertyVarSet::setOkEnabled(bool enabled)
{
QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
okButton->setEnabled(enabled);
}
void DlgAddPropertyVarSet::clearEditors()
{
bool beforeBlocked = ui->lineEditName->blockSignals(true);
ui->lineEditName->clear();
ui->lineEditName->blockSignals(beforeBlocked);
removeEditor();
setOkEnabled(false);
namePropertyToAdd.clear();
editor = nullptr;
}
void DlgAddPropertyVarSet::removeEditor()
{
if (editor) {
layout()->removeWidget(editor.get());
QWidget::setTabOrder(ui->comboBoxType, ui->checkBoxAdd);
// FC_ERR("remove editor");
// printFocusChain(ui->comboBoxType);
}
}
static PropertyEditor::PropertyItem *createPropertyItem(App::Property *prop)
{
const char *editor = prop->getEditorName();
if (!editor || !editor[0]) {
return nullptr;
}
auto item = static_cast<PropertyEditor::PropertyItem*>(
PropertyEditor::PropertyItemFactory::instance().createPropertyItem(editor));
if (!item) {
qWarning("No property item for type %s found\n", editor);
}
return item;
}
void DlgAddPropertyVarSet::addEditor(PropertyEditor::PropertyItem* propertyItem, std::string& /*type*/)
{
editor.reset(propertyItem->createEditor(this, [this]() {
this->valueChanged();
}));
editor->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
editor->setObjectName(QString::fromUtf8("editor"));
auto formLayout = qobject_cast<QFormLayout*>(layout());
formLayout->setWidget(3, QFormLayout::FieldRole, editor.get());
QWidget::setTabOrder(ui->comboBoxType, editor.get());
QWidget::setTabOrder(editor.get(), ui->checkBoxAdd);
// FC_ERR("add editor");
// printFocusChain(editor.get());
}
bool DlgAddPropertyVarSet::isSupportedType(std::string& type)
{
return unsupportedTypes.find(type) == unsupportedTypes.end();
}
void DlgAddPropertyVarSet::createProperty(std::string& name, std::string& group)
{
std::string type = ui->comboBoxType->currentText().toUtf8().constData();
App::Property* prop;
try {
prop = varSet->addDynamicProperty(type.c_str(), name.c_str(),
group.c_str());
}
catch (Base::Exception& e) {
e.ReportException();
QMessageBox::critical(this,
QObject::tr("Add property"),
QObject::tr("Failed to add property to '%1': %2").arg(
QString::fromLatin1(varSet->getFullName().c_str()),
QString::fromUtf8(e.what())));
clearEditors();
return;
}
namePropertyToAdd = name;
objectIdentifier = std::make_unique<App::ObjectIdentifier>(*prop);
// creating a propertyItem here because it has all kinds of logic for
// editors that we can reuse
removeEditor();
propertyItem.reset(createPropertyItem(prop));
if (propertyItem && isSupportedType(type)) {
propertyItem->setPropertyData({prop});
propertyItem->bind(*objectIdentifier);
addEditor(propertyItem.get(), type);
}
setOkEnabled(true);
}
void DlgAddPropertyVarSet::onNamePropertyDetermined(const QString& text)
{
if (!namePropertyToAdd.empty()) {
// We were already adding a property with this name. We have to remove
// the property, the editor because it is associated with the property,
// and we have to abort the transaction.
varSet->removeDynamicProperty(namePropertyToAdd.c_str());
removeEditor();
App::Document* doc = varSet->getDocument();
if (doc->hasPendingTransaction()) {
doc->abortTransaction();
}
namePropertyToAdd.clear();
}
if (text.isEmpty()) {
// We can not define a property, so we should not have an editor for the value.
clearEditors();
return;
}
std::string name = text.toUtf8().constData();
std::string group = comboBoxGroup.currentText().toUtf8().constData();
if(name.empty() || group.empty()
|| name != Base::Tools::getIdentifier(name)
|| group != Base::Tools::getIdentifier(group)) {
QMessageBox::critical(getMainWindow(),
QObject::tr("Invalid name"),
QObject::tr("The property name or group name must only contain alpha numericals,\n"
"underscore, and must not start with a digit."));
clearEditors();
return;
}
auto prop = varSet->getPropertyByName(name.c_str());
if(prop && prop->getContainer() == varSet) {
QMessageBox::critical(this,
QObject::tr("Invalid name"),
QObject::tr("The property '%1' already exists in '%2'").arg(
QString::fromLatin1(name.c_str()),
QString::fromLatin1(varSet->getFullName().c_str())));
clearEditors();
return;
}
App::Document* doc = varSet->getDocument();
doc->openTransaction("Add property VarSet");
createProperty(name, group);
// FC_ERR("chain onNameDetermined");
// printFocusChain(ui->lineEditName);
}
void DlgAddPropertyVarSet::onGroupDetermined()
{
std::string group = comboBoxGroup.currentText().toUtf8().constData();
if (group.empty() || group != Base::Tools::getIdentifier(group)) {
QMessageBox::critical(this,
QObject::tr("Invalid name"),
QObject::tr("The group name must only contain alpha numericals,\n"
"underscore, and must not start with a digit."));
comboBoxGroup.setEditText(QString::fromUtf8("Base"));
return;
}
if (!namePropertyToAdd.empty()) {
// we were already adding a property
App::Property* prop = varSet->getPropertyByName(namePropertyToAdd.c_str());
if (prop->getGroup() != group) {
varSet->changeDynamicProperty(prop, group.c_str(), nullptr);
}
}
// FC_ERR("chain onGroupDetermined");
// printFocusChain(&comboBoxGroup);
ui->comboBoxType->setFocus();
}
void DlgAddPropertyVarSet::onTypePropertyDetermined()
{
std::string type = ui->comboBoxType->currentText().toUtf8().constData();
if (!namePropertyToAdd.empty()) {
// we were already adding a name, so check this property
App::Property* prop = varSet->getPropertyByName(namePropertyToAdd.c_str());
if (prop->getTypeId() != Base::Type::fromName(type.c_str())) {
// the property should have a different type
std::string group = prop->getGroup();
varSet->removeDynamicProperty(namePropertyToAdd.c_str());
createProperty(namePropertyToAdd, group);
}
}
}
void DlgAddPropertyVarSet::valueChanged()
{
QVariant data;
data = propertyItem->editorData(editor.get());
propertyItem->setData(data);
}
void DlgAddPropertyVarSet::accept()
{
App::Document* doc = varSet->getDocument();
doc->commitTransaction();
if (ui->checkBoxAdd->isChecked()) {
clearEditors();
doc->openTransaction();
ui->lineEditName->setFocus();
return;
}
std::string group = comboBoxGroup.currentText().toUtf8().constData();
std::string type = ui->comboBoxType->currentText().toUtf8().constData();
auto paramGroup = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/PropertyView");
paramGroup->SetASCII("NewPropertyType", type.c_str());
paramGroup->SetASCII("NewPropertyGroup", group.c_str());
QDialog::accept();
}
void DlgAddPropertyVarSet::reject()
{
App::Document* doc = varSet->getDocument();
// a transaction is not pending if a name has not been determined.
if (doc->hasPendingTransaction()) {
doc->abortTransaction();
}
QDialog::reject();
}
#include "moc_DlgAddPropertyVarSet.cpp"