mirror of
https://github.com/qgis/QGIS.git
synced 2025-12-30 00:29:39 -05:00
3306 lines
111 KiB
C++
3306 lines
111 KiB
C++
/***************************************************************************
|
|
qgsattributeform.cpp
|
|
--------------------------------------
|
|
Date : 3.5.2014
|
|
Copyright : (C) 2014 Matthias Kuhn
|
|
Email : matthias at opengis dot ch
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "qgsattributeform.h"
|
|
|
|
#include "qgsattributeeditorspacerelement.h"
|
|
#include "qgsattributeforminterface.h"
|
|
#include "qgsattributeformlegacyinterface.h"
|
|
#include "qgsattributeformrelationeditorwidget.h"
|
|
#include "qgsattributeeditoraction.h"
|
|
#include "qgsattributeeditorcontainer.h"
|
|
#include "qgsattributeeditorfield.h"
|
|
#include "qgsattributeeditorrelation.h"
|
|
#include "qgsattributeeditorqmlelement.h"
|
|
#include "qgsattributeeditorhtmlelement.h"
|
|
#include "qgsattributeeditortextelement.h"
|
|
#include "qgseditorwidgetregistry.h"
|
|
#include "qgsfeatureiterator.h"
|
|
#include "qgsgui.h"
|
|
#include "qgsproject.h"
|
|
#include "qgspythonrunner.h"
|
|
#include "qgsrelationwidgetwrapper.h"
|
|
#include "qgstextwidgetwrapper.h"
|
|
#include "qgsvectordataprovider.h"
|
|
#include "qgsattributeformeditorwidget.h"
|
|
#include "qgsmessagebar.h"
|
|
#include "qgsmessagebaritem.h"
|
|
#include "qgsnetworkcontentfetcherregistry.h"
|
|
#include "qgseditorwidgetwrapper.h"
|
|
#include "qgsrelationmanager.h"
|
|
#include "qgslogger.h"
|
|
#include "qgstabwidget.h"
|
|
#include "qgsscrollarea.h"
|
|
#include "qgsvectorlayerjoinbuffer.h"
|
|
#include "qgsvectorlayertoolscontext.h"
|
|
#include "qgsvectorlayerutils.h"
|
|
#include "qgsactionwidgetwrapper.h"
|
|
#include "qgsqmlwidgetwrapper.h"
|
|
#include "qgshtmlwidgetwrapper.h"
|
|
#include "qgsspacerwidgetwrapper.h"
|
|
#include "qgsapplication.h"
|
|
#include "qgsexpressioncontextutils.h"
|
|
#include "qgsfeaturerequest.h"
|
|
#include "qgstexteditwrapper.h"
|
|
#include "qgsfieldmodel.h"
|
|
#include "qgscollapsiblegroupbox.h"
|
|
|
|
#include <QDir>
|
|
#include <QTextStream>
|
|
#include <QFileInfo>
|
|
#include <QFile>
|
|
#include <QFormLayout>
|
|
#include <QGridLayout>
|
|
#include <QKeyEvent>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QUiLoader>
|
|
#include <QMessageBox>
|
|
#include <QToolButton>
|
|
#include <QMenu>
|
|
#include <QSvgWidget>
|
|
|
|
int QgsAttributeForm::sFormCounter = 0;
|
|
|
|
QgsAttributeForm::QgsAttributeForm( QgsVectorLayer *vl, const QgsFeature &feature, const QgsAttributeEditorContext &context, QWidget *parent )
|
|
: QWidget( parent )
|
|
, mLayer( vl )
|
|
, mOwnsMessageBar( true )
|
|
, mContext( context )
|
|
, mFormNr( sFormCounter++ )
|
|
, mIsSaving( false )
|
|
, mPreventFeatureRefresh( false )
|
|
, mIsSettingMultiEditFeatures( false )
|
|
, mUnsavedMultiEditChanges( false )
|
|
, mEditCommandMessage( tr( "Attributes changed" ) )
|
|
, mMode( QgsAttributeEditorContext::SingleEditMode )
|
|
{
|
|
init();
|
|
initPython();
|
|
setFeature( feature );
|
|
|
|
connect( vl, &QgsVectorLayer::updatedFields, this, &QgsAttributeForm::onUpdatedFields );
|
|
connect( vl, &QgsVectorLayer::beforeAddingExpressionField, this, &QgsAttributeForm::preventFeatureRefresh );
|
|
connect( vl, &QgsVectorLayer::beforeRemovingExpressionField, this, &QgsAttributeForm::preventFeatureRefresh );
|
|
connect( vl, &QgsVectorLayer::selectionChanged, this, &QgsAttributeForm::layerSelectionChanged );
|
|
connect( this, &QgsAttributeForm::modeChanged, this, &QgsAttributeForm::updateContainersVisibility );
|
|
|
|
updateContainersVisibility();
|
|
updateLabels();
|
|
updateEditableState();
|
|
|
|
}
|
|
|
|
QgsAttributeForm::~QgsAttributeForm()
|
|
{
|
|
cleanPython();
|
|
qDeleteAll( mInterfaces );
|
|
}
|
|
|
|
void QgsAttributeForm::hideButtonBox()
|
|
{
|
|
mButtonBox->hide();
|
|
|
|
// Make sure that changes are taken into account if somebody tries to figure out if there have been some
|
|
if ( mMode == QgsAttributeEditorContext::SingleEditMode )
|
|
connect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
|
|
}
|
|
|
|
void QgsAttributeForm::showButtonBox()
|
|
{
|
|
mButtonBox->show();
|
|
|
|
disconnect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
|
|
}
|
|
|
|
void QgsAttributeForm::disconnectButtonBox()
|
|
{
|
|
disconnect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
|
|
disconnect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
|
|
}
|
|
|
|
void QgsAttributeForm::addInterface( QgsAttributeFormInterface *iface )
|
|
{
|
|
mInterfaces.append( iface );
|
|
}
|
|
|
|
bool QgsAttributeForm::editable()
|
|
{
|
|
return mFeature.isValid() && mLayer->isEditable();
|
|
}
|
|
|
|
void QgsAttributeForm::setMode( QgsAttributeEditorContext::Mode mode )
|
|
{
|
|
if ( mode == mMode )
|
|
return;
|
|
|
|
if ( mMode == QgsAttributeEditorContext::MultiEditMode )
|
|
{
|
|
//switching out of multi edit mode triggers a save
|
|
if ( mUnsavedMultiEditChanges )
|
|
{
|
|
// prompt for save
|
|
int res = QMessageBox::question( this, tr( "Multiedit Attributes" ),
|
|
tr( "Apply changes to edited features?" ), QMessageBox::Yes | QMessageBox::No );
|
|
if ( res == QMessageBox::Yes )
|
|
{
|
|
save();
|
|
}
|
|
}
|
|
clearMultiEditMessages();
|
|
}
|
|
mUnsavedMultiEditChanges = false;
|
|
|
|
mMode = mode;
|
|
|
|
if ( mButtonBox->isVisible() && mMode == QgsAttributeEditorContext::SingleEditMode )
|
|
{
|
|
connect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
|
|
}
|
|
else
|
|
{
|
|
disconnect( mLayer, &QgsVectorLayer::beforeModifiedCheck, this, &QgsAttributeForm::save );
|
|
}
|
|
|
|
//update all form editor widget modes to match
|
|
for ( QgsAttributeFormWidget *w : std::as_const( mFormWidgets ) )
|
|
{
|
|
switch ( mode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
w->setMode( QgsAttributeFormWidget::DefaultMode );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
w->setMode( QgsAttributeFormWidget::DefaultMode );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
w->setMode( QgsAttributeFormWidget::DefaultMode );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
w->setMode( QgsAttributeFormWidget::MultiEditMode );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
w->setMode( QgsAttributeFormWidget::SearchMode );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
w->setMode( QgsAttributeFormWidget::AggregateSearchMode );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
w->setMode( QgsAttributeFormWidget::DefaultMode );
|
|
break;
|
|
}
|
|
}
|
|
//update all form editor widget modes to match
|
|
for ( QgsWidgetWrapper *w : std::as_const( mWidgets ) )
|
|
{
|
|
QgsAttributeEditorContext newContext = w->context();
|
|
newContext.setAttributeFormMode( mMode );
|
|
w->setContext( newContext );
|
|
}
|
|
|
|
bool relationWidgetsVisible = ( mMode != QgsAttributeEditorContext::AggregateSearchMode );
|
|
for ( QgsAttributeFormRelationEditorWidget *w : findChildren< QgsAttributeFormRelationEditorWidget * >() )
|
|
{
|
|
w->setVisible( relationWidgetsVisible );
|
|
}
|
|
|
|
switch ( mode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
setFeature( mFeature );
|
|
mSearchButtonBox->setVisible( false );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
synchronizeState();
|
|
mSearchButtonBox->setVisible( false );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
synchronizeState();
|
|
mSearchButtonBox->setVisible( false );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
resetMultiEdit( false );
|
|
synchronizeState();
|
|
mSearchButtonBox->setVisible( false );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
mSearchButtonBox->setVisible( true );
|
|
synchronizeState();
|
|
hideButtonBox();
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
mSearchButtonBox->setVisible( false );
|
|
synchronizeState();
|
|
hideButtonBox();
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
setFeature( mFeature );
|
|
synchronizeState();
|
|
mSearchButtonBox->setVisible( false );
|
|
break;
|
|
}
|
|
|
|
emit modeChanged( mMode );
|
|
}
|
|
|
|
void QgsAttributeForm::changeAttribute( const QString &field, const QVariant &value, const QString &hintText )
|
|
{
|
|
const auto constMWidgets = mWidgets;
|
|
for ( QgsWidgetWrapper *ww : constMWidgets )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
if ( eww->field().name() == field )
|
|
{
|
|
eww->setValues( value, QVariantList() );
|
|
eww->setHint( hintText );
|
|
}
|
|
// see if the field is present in additional fields of the editor widget
|
|
int index = eww->additionalFields().indexOf( field );
|
|
if ( index >= 0 )
|
|
{
|
|
QVariant mainValue = eww->value();
|
|
QVariantList additionalFieldValues = eww->additionalFieldValues();
|
|
additionalFieldValues[index] = value;
|
|
eww->setValues( mainValue, additionalFieldValues );
|
|
eww->setHint( hintText );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::changeGeometry( const QgsGeometry &geometry )
|
|
{
|
|
mFeature.setGeometry( geometry );
|
|
}
|
|
|
|
void QgsAttributeForm::setFeature( const QgsFeature &feature )
|
|
{
|
|
mIsSettingFeature = true;
|
|
mFeature = feature;
|
|
mCurrentFormFeature = feature;
|
|
|
|
switch ( mMode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
{
|
|
resetValues();
|
|
|
|
synchronizeState();
|
|
|
|
// Settings of feature is done when we trigger the attribute form interface
|
|
// Issue https://github.com/qgis/QGIS/issues/29667
|
|
mIsSettingFeature = false;
|
|
const auto constMInterfaces = mInterfaces;
|
|
for ( QgsAttributeFormInterface *iface : constMInterfaces )
|
|
{
|
|
iface->featureChanged();
|
|
}
|
|
break;
|
|
}
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
{
|
|
resetValues();
|
|
break;
|
|
}
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
{
|
|
//ignore setFeature
|
|
break;
|
|
}
|
|
}
|
|
mIsSettingFeature = false;
|
|
}
|
|
|
|
bool QgsAttributeForm::saveEdits( QString *error )
|
|
{
|
|
bool success = true;
|
|
bool changedLayer = false;
|
|
|
|
QgsFeature updatedFeature = QgsFeature( mFeature );
|
|
if ( mFeature.isValid() || mMode == QgsAttributeEditorContext::AddFeatureMode )
|
|
{
|
|
bool doUpdate = false;
|
|
|
|
// An add dialog should perform an action by default
|
|
// and not only if attributes have "changed"
|
|
if ( mMode == QgsAttributeEditorContext::AddFeatureMode || mMode == QgsAttributeEditorContext::FixAttributeMode )
|
|
{
|
|
doUpdate = true;
|
|
}
|
|
|
|
QgsAttributes src = mFeature.attributes();
|
|
QgsAttributes dst = mFeature.attributes();
|
|
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
// check for invalid JSON values
|
|
QgsTextEditWrapper *textEdit = qobject_cast<QgsTextEditWrapper *>( eww );
|
|
if ( textEdit && textEdit->isInvalidJSON() )
|
|
{
|
|
if ( error )
|
|
*error = tr( "JSON value for %1 is invalid and has not been saved" ).arg( eww->field().name() );
|
|
return false;
|
|
}
|
|
QVariantList dstVars = QVariantList() << dst.at( eww->fieldIdx() );
|
|
QVariantList srcVars = QVariantList() << eww->value();
|
|
QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
|
|
|
|
// append additional fields
|
|
const QStringList additionalFields = eww->additionalFields();
|
|
for ( const QString &fieldName : additionalFields )
|
|
{
|
|
int idx = eww->layer()->fields().lookupField( fieldName );
|
|
fieldIndexes << idx;
|
|
dstVars << dst.at( idx );
|
|
}
|
|
srcVars.append( eww->additionalFieldValues() );
|
|
|
|
Q_ASSERT( dstVars.count() == srcVars.count() );
|
|
|
|
for ( int i = 0; i < dstVars.count(); i++ )
|
|
{
|
|
|
|
if ( !qgsVariantEqual( dstVars[i], srcVars[i] ) && srcVars[i].isValid() )
|
|
{
|
|
dst[fieldIndexes[i]] = srcVars[i];
|
|
|
|
doUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
updatedFeature.setAttributes( dst );
|
|
|
|
const auto constMInterfaces = mInterfaces;
|
|
for ( QgsAttributeFormInterface *iface : constMInterfaces )
|
|
{
|
|
if ( !iface->acceptChanges( updatedFeature ) )
|
|
{
|
|
doUpdate = false;
|
|
}
|
|
}
|
|
|
|
if ( doUpdate )
|
|
{
|
|
if ( mMode == QgsAttributeEditorContext::FixAttributeMode )
|
|
{
|
|
mFeature = updatedFeature;
|
|
}
|
|
else if ( mMode == QgsAttributeEditorContext::AddFeatureMode )
|
|
{
|
|
mFeature.setValid( true );
|
|
mLayer->beginEditCommand( mEditCommandMessage );
|
|
bool res = mLayer->addFeature( updatedFeature );
|
|
if ( res )
|
|
{
|
|
mFeature.setAttributes( updatedFeature.attributes() );
|
|
mLayer->endEditCommand();
|
|
setMode( QgsAttributeEditorContext::SingleEditMode );
|
|
changedLayer = true;
|
|
}
|
|
else
|
|
mLayer->destroyEditCommand();
|
|
}
|
|
else
|
|
{
|
|
mLayer->beginEditCommand( mEditCommandMessage );
|
|
|
|
QgsAttributeMap newValues;
|
|
QgsAttributeMap oldValues;
|
|
|
|
int n = 0;
|
|
for ( int i = 0; i < dst.count(); ++i )
|
|
{
|
|
if ( qgsVariantEqual( dst.at( i ), src.at( i ) ) // If field is not changed...
|
|
|| !dst.at( i ).isValid() // or the widget returns invalid (== do not change)
|
|
|| !fieldIsEditable( i ) ) // or the field cannot be edited ...
|
|
{
|
|
continue;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Updating field %1" ).arg( i ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "dst:'%1' (type:%2, isNull:%3, isValid:%4)" )
|
|
.arg( dst.at( i ).toString(), dst.at( i ).typeName() ).arg( QgsVariantUtils::isNull( dst.at( i ) ) ).arg( dst.at( i ).isValid() ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "src:'%1' (type:%2, isNull:%3, isValid:%4)" )
|
|
.arg( src.at( i ).toString(), src.at( i ).typeName() ).arg( QgsVariantUtils::isNull( src.at( i ) ) ).arg( src.at( i ).isValid() ), 2 );
|
|
|
|
newValues[i] = dst.at( i );
|
|
oldValues[i] = src.at( i );
|
|
|
|
n++;
|
|
}
|
|
|
|
std::unique_ptr<QgsVectorLayerToolsContext> context = std::make_unique<QgsVectorLayerToolsContext>();
|
|
QgsExpressionContext expressionContext = createExpressionContext( updatedFeature );
|
|
context->setExpressionContext( &expressionContext );
|
|
success = mLayer->changeAttributeValues( mFeature.id(), newValues, oldValues, false, context.get() );
|
|
|
|
if ( success && n > 0 )
|
|
{
|
|
mLayer->endEditCommand();
|
|
mFeature.setAttributes( dst );
|
|
changedLayer = true;
|
|
}
|
|
else
|
|
{
|
|
mLayer->destroyEditCommand();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
emit featureSaved( updatedFeature );
|
|
|
|
// [MD] Refresh canvas only when absolutely necessary - it interferes with other stuff (#11361).
|
|
// This code should be revisited - and the signals should be fired (+ layer repainted)
|
|
// only when actually doing any changes. I am unsure if it is actually a good idea
|
|
// to call save() whenever some code asks for vector layer's modified status
|
|
// (which is the case when attribute table is open)
|
|
if ( changedLayer )
|
|
mLayer->triggerRepaint();
|
|
|
|
return success;
|
|
}
|
|
|
|
QgsFeature QgsAttributeForm::getUpdatedFeature() const
|
|
{
|
|
// create updated Feature
|
|
QgsFeature updatedFeature = QgsFeature( mFeature );
|
|
|
|
QgsAttributes featureAttributes = mFeature.attributes();
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( !eww )
|
|
continue;
|
|
|
|
QVariantList dstVars = QVariantList() << featureAttributes.at( eww->fieldIdx() );
|
|
QVariantList srcVars = QVariantList() << eww->value();
|
|
QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
|
|
|
|
// append additional fields
|
|
const QStringList additionalFields = eww->additionalFields();
|
|
for ( const QString &fieldName : additionalFields )
|
|
{
|
|
int idx = eww->layer()->fields().lookupField( fieldName );
|
|
fieldIndexes << idx;
|
|
dstVars << featureAttributes.at( idx );
|
|
}
|
|
srcVars.append( eww->additionalFieldValues() );
|
|
|
|
Q_ASSERT( dstVars.count() == srcVars.count() );
|
|
|
|
for ( int i = 0; i < dstVars.count(); i++ )
|
|
{
|
|
if ( !qgsVariantEqual( dstVars[i], srcVars[i] ) && srcVars[i].isValid() )
|
|
featureAttributes[fieldIndexes[i]] = srcVars[i];
|
|
}
|
|
}
|
|
updatedFeature.setAttributes( featureAttributes );
|
|
|
|
return updatedFeature;
|
|
}
|
|
|
|
void QgsAttributeForm::updateValuesDependencies( const int originIdx )
|
|
{
|
|
updateValuesDependenciesDefaultValues( originIdx );
|
|
updateValuesDependenciesVirtualFields( originIdx );
|
|
}
|
|
|
|
void QgsAttributeForm::updateValuesDependenciesDefaultValues( const int originIdx )
|
|
{
|
|
if ( !mDefaultValueDependencies.contains( originIdx ) )
|
|
return;
|
|
|
|
if ( !mFeature.isValid()
|
|
&& mMode != QgsAttributeEditorContext::AddFeatureMode )
|
|
return;
|
|
|
|
// create updated Feature
|
|
QgsFeature updatedFeature = getUpdatedFeature();
|
|
|
|
// go through depending fields and update the fields with defaultexpression
|
|
QList<QgsWidgetWrapper *> relevantWidgets = mDefaultValueDependencies.values( originIdx );
|
|
for ( QgsWidgetWrapper *ww : std::as_const( relevantWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
// Update only on form opening (except when applyOnUpdate is activated)
|
|
if ( mValuesInitialized && !eww->field().defaultValueDefinition().applyOnUpdate() )
|
|
continue;
|
|
|
|
// Update only when mMode is AddFeatureMode (except when applyOnUpdate is activated)
|
|
if ( mMode != QgsAttributeEditorContext::AddFeatureMode && !eww->field().defaultValueDefinition().applyOnUpdate() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//do not update when this widget is already updating (avoid recursions)
|
|
if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
|
|
continue;
|
|
|
|
QgsExpressionContext context = createExpressionContext( updatedFeature );
|
|
|
|
const QVariant value = mLayer->defaultValue( eww->fieldIdx(), updatedFeature, &context );
|
|
eww->setValue( value );
|
|
mCurrentFormFeature.setAttribute( eww->field().name(), value );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateValuesDependenciesVirtualFields( const int originIdx )
|
|
{
|
|
if ( !mVirtualFieldsDependencies.contains( originIdx ) )
|
|
return;
|
|
|
|
if ( !mFeature.isValid() )
|
|
return;
|
|
|
|
// create updated Feature
|
|
QgsFeature updatedFeature = getUpdatedFeature();
|
|
|
|
// go through depending fields and update the virtual field with its expression
|
|
const QList<QgsWidgetWrapper *> relevantWidgets = mVirtualFieldsDependencies.values( originIdx );
|
|
for ( QgsWidgetWrapper *ww : relevantWidgets )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( !eww )
|
|
continue;
|
|
|
|
//do not update when this widget is already updating (avoid recursions)
|
|
if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
|
|
continue;
|
|
|
|
// Update value
|
|
QgsExpressionContext context = createExpressionContext( updatedFeature );
|
|
QgsExpression exp( mLayer->expressionField( eww->fieldIdx() ) );
|
|
const QVariant value = exp.evaluate( &context );
|
|
updatedFeature.setAttribute( eww->fieldIdx(), value );
|
|
eww->setValue( value );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateRelatedLayerFields()
|
|
{
|
|
// Synchronize dependencies
|
|
updateRelatedLayerFieldsDependencies();
|
|
|
|
if ( mRelatedLayerFieldsDependencies.isEmpty() )
|
|
return;
|
|
|
|
if ( !mFeature.isValid() )
|
|
return;
|
|
|
|
// create updated Feature
|
|
QgsFeature updatedFeature = getUpdatedFeature();
|
|
|
|
// go through depending fields and update the fields with virtual field
|
|
const QSet<QgsEditorWidgetWrapper *> relevantWidgets = mRelatedLayerFieldsDependencies;
|
|
for ( QgsEditorWidgetWrapper *eww : relevantWidgets )
|
|
{
|
|
//do not update when this widget is already updating (avoid recursions)
|
|
if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
|
|
continue;
|
|
|
|
// Update value
|
|
QgsExpressionContext context = createExpressionContext( updatedFeature );
|
|
QgsExpression exp( mLayer->expressionField( eww->fieldIdx() ) );
|
|
QVariant value = exp.evaluate( &context );
|
|
eww->setValue( value );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::resetMultiEdit( bool promptToSave )
|
|
{
|
|
if ( promptToSave )
|
|
save();
|
|
|
|
mUnsavedMultiEditChanges = false;
|
|
setMultiEditFeatureIds( mLayer->selectedFeatureIds() );
|
|
}
|
|
|
|
void QgsAttributeForm::multiEditMessageClicked( const QString &link )
|
|
{
|
|
clearMultiEditMessages();
|
|
resetMultiEdit( link == QLatin1String( "#apply" ) );
|
|
}
|
|
|
|
void QgsAttributeForm::filterTriggered()
|
|
{
|
|
QString filter = createFilterExpression();
|
|
emit filterExpressionSet( filter, ReplaceFilter );
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
|
|
setMode( QgsAttributeEditorContext::SingleEditMode );
|
|
}
|
|
|
|
void QgsAttributeForm::searchZoomTo()
|
|
{
|
|
QString filter = createFilterExpression();
|
|
if ( filter.isEmpty() )
|
|
return;
|
|
|
|
emit zoomToFeatures( filter );
|
|
}
|
|
|
|
void QgsAttributeForm::searchFlash()
|
|
{
|
|
QString filter = createFilterExpression();
|
|
if ( filter.isEmpty() )
|
|
return;
|
|
|
|
emit flashFeatures( filter );
|
|
}
|
|
|
|
void QgsAttributeForm::filterAndTriggered()
|
|
{
|
|
QString filter = createFilterExpression();
|
|
if ( filter.isEmpty() )
|
|
return;
|
|
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
|
|
setMode( QgsAttributeEditorContext::SingleEditMode );
|
|
emit filterExpressionSet( filter, FilterAnd );
|
|
}
|
|
|
|
void QgsAttributeForm::filterOrTriggered()
|
|
{
|
|
QString filter = createFilterExpression();
|
|
if ( filter.isEmpty() )
|
|
return;
|
|
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
|
|
setMode( QgsAttributeEditorContext::SingleEditMode );
|
|
emit filterExpressionSet( filter, FilterOr );
|
|
}
|
|
|
|
void QgsAttributeForm::pushSelectedFeaturesMessage()
|
|
{
|
|
int count = mLayer->selectedFeatureCount();
|
|
if ( count > 0 )
|
|
{
|
|
mMessageBar->pushMessage( QString(),
|
|
tr( "%n matching feature(s) selected", "matching features", count ),
|
|
Qgis::MessageLevel::Info );
|
|
}
|
|
else
|
|
{
|
|
mMessageBar->pushMessage( QString(),
|
|
tr( "No matching features found" ),
|
|
Qgis::MessageLevel::Info );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::displayWarning( const QString &message )
|
|
{
|
|
mMessageBar->pushMessage( QString(),
|
|
message,
|
|
Qgis::MessageLevel::Warning );
|
|
}
|
|
|
|
void QgsAttributeForm::runSearchSelect( Qgis::SelectBehavior behavior )
|
|
{
|
|
QString filter = createFilterExpression();
|
|
if ( filter.isEmpty() )
|
|
return;
|
|
|
|
mLayer->selectByExpression( filter, behavior );
|
|
pushSelectedFeaturesMessage();
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
|
|
setMode( QgsAttributeEditorContext::SingleEditMode );
|
|
}
|
|
|
|
void QgsAttributeForm::searchSetSelection()
|
|
{
|
|
runSearchSelect( Qgis::SelectBehavior::SetSelection );
|
|
}
|
|
|
|
void QgsAttributeForm::searchAddToSelection()
|
|
{
|
|
runSearchSelect( Qgis::SelectBehavior::AddToSelection );
|
|
}
|
|
|
|
void QgsAttributeForm::searchRemoveFromSelection()
|
|
{
|
|
runSearchSelect( Qgis::SelectBehavior::RemoveFromSelection );
|
|
}
|
|
|
|
void QgsAttributeForm::searchIntersectSelection()
|
|
{
|
|
runSearchSelect( Qgis::SelectBehavior::IntersectSelection );
|
|
}
|
|
|
|
bool QgsAttributeForm::saveMultiEdits()
|
|
{
|
|
//find changed attributes
|
|
QgsAttributeMap newAttributeValues;
|
|
const QList<int> fieldIndexes = mFormEditorWidgets.uniqueKeys();
|
|
mFormEditorWidgets.constBegin();
|
|
for ( int fieldIndex : fieldIndexes )
|
|
{
|
|
const QList<QgsAttributeFormEditorWidget *> widgets = mFormEditorWidgets.values( fieldIndex );
|
|
if ( !widgets.first()->hasChanged() )
|
|
continue;
|
|
|
|
if ( !widgets.first()->currentValue().isValid() // if the widget returns invalid (== do not change)
|
|
|| !fieldIsEditable( fieldIndex ) ) // or the field cannot be edited ...
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// let editor know we've accepted the changes
|
|
for ( QgsAttributeFormEditorWidget *widget : widgets )
|
|
widget->changesCommitted();
|
|
|
|
newAttributeValues.insert( fieldIndex, widgets.first()->currentValue() );
|
|
}
|
|
|
|
if ( newAttributeValues.isEmpty() )
|
|
{
|
|
//nothing to change
|
|
return true;
|
|
}
|
|
|
|
#if 0
|
|
// prompt for save
|
|
int res = QMessageBox::information( this, tr( "Multiedit Attributes" ),
|
|
tr( "Edits will be applied to all selected features." ), QMessageBox::Ok | QMessageBox::Cancel );
|
|
if ( res != QMessageBox::Ok )
|
|
{
|
|
resetMultiEdit();
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
mLayer->beginEditCommand( tr( "Updated multiple feature attributes" ) );
|
|
|
|
bool success = true;
|
|
|
|
const auto constMultiEditFeatureIds = mMultiEditFeatureIds;
|
|
for ( QgsFeatureId fid : constMultiEditFeatureIds )
|
|
{
|
|
QgsAttributeMap::const_iterator aIt = newAttributeValues.constBegin();
|
|
for ( ; aIt != newAttributeValues.constEnd(); ++aIt )
|
|
{
|
|
success &= mLayer->changeAttributeValue( fid, aIt.key(), aIt.value() );
|
|
}
|
|
}
|
|
|
|
clearMultiEditMessages();
|
|
if ( success )
|
|
{
|
|
mLayer->endEditCommand();
|
|
mLayer->triggerRepaint();
|
|
mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Attribute changes for multiple features applied." ), Qgis::MessageLevel::Success, -1 );
|
|
}
|
|
else
|
|
{
|
|
mLayer->destroyEditCommand();
|
|
mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Changes could not be applied." ), Qgis::MessageLevel::Warning, 0 );
|
|
}
|
|
|
|
if ( !mButtonBox->isVisible() )
|
|
mMessageBar->pushItem( mMultiEditMessageBarItem );
|
|
return success;
|
|
}
|
|
|
|
bool QgsAttributeForm::save()
|
|
{
|
|
return saveWithDetails( nullptr );
|
|
}
|
|
|
|
bool QgsAttributeForm::saveWithDetails( QString *error )
|
|
{
|
|
if ( error )
|
|
error->clear();
|
|
|
|
if ( mIsSaving )
|
|
return true;
|
|
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed && !mValidConstraints )
|
|
{
|
|
// the feature isn't saved (as per the warning provided), but we return true
|
|
// so switching features still works
|
|
return true;
|
|
}
|
|
|
|
for ( QgsWidgetWrapper *wrapper : std::as_const( mWidgets ) )
|
|
{
|
|
wrapper->notifyAboutToSave();
|
|
}
|
|
|
|
// only do the dirty checks when editing an existing feature - for new
|
|
// features we need to add them even if the attributes are unchanged from the initial
|
|
// default values
|
|
switch ( mMode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
if ( !mDirty )
|
|
return true;
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
break;
|
|
}
|
|
|
|
mIsSaving = true;
|
|
|
|
bool success = true;
|
|
|
|
emit beforeSave( success );
|
|
|
|
// Somebody wants to prevent this form from saving
|
|
if ( !success )
|
|
return false;
|
|
|
|
switch ( mMode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
success = saveEdits( error );
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
success = saveMultiEdits();
|
|
break;
|
|
}
|
|
|
|
mIsSaving = false;
|
|
mUnsavedMultiEditChanges = false;
|
|
mDirty = false;
|
|
|
|
return success;
|
|
}
|
|
|
|
|
|
void QgsAttributeForm::resetValues()
|
|
{
|
|
mValuesInitialized = false;
|
|
const auto constMWidgets = mWidgets;
|
|
for ( QgsWidgetWrapper *ww : constMWidgets )
|
|
{
|
|
ww->setFeature( mFeature );
|
|
}
|
|
|
|
// Update dependent virtual fields (not default values / not referencing layer values)
|
|
for ( QgsWidgetWrapper *ww : constMWidgets )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( !eww )
|
|
continue;
|
|
|
|
// Append field index here, so it's not updated recursively
|
|
mAlreadyUpdatedFields.append( eww->fieldIdx() );
|
|
updateValuesDependenciesVirtualFields( eww->fieldIdx() );
|
|
mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );
|
|
}
|
|
|
|
mValuesInitialized = true;
|
|
mDirty = false;
|
|
}
|
|
|
|
void QgsAttributeForm::resetSearch()
|
|
{
|
|
const auto widgets { findChildren< QgsAttributeFormEditorWidget * >() };
|
|
for ( QgsAttributeFormEditorWidget *w : widgets )
|
|
{
|
|
w->resetSearch();
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::clearMultiEditMessages()
|
|
{
|
|
if ( mMultiEditUnsavedMessageBarItem )
|
|
{
|
|
if ( !mButtonBox->isVisible() )
|
|
mMessageBar->popWidget( mMultiEditUnsavedMessageBarItem );
|
|
mMultiEditUnsavedMessageBarItem = nullptr;
|
|
}
|
|
if ( mMultiEditMessageBarItem )
|
|
{
|
|
if ( !mButtonBox->isVisible() )
|
|
mMessageBar->popWidget( mMultiEditMessageBarItem );
|
|
mMultiEditMessageBarItem = nullptr;
|
|
}
|
|
}
|
|
|
|
QString QgsAttributeForm::createFilterExpression() const
|
|
{
|
|
QStringList filters;
|
|
for ( QgsAttributeFormWidget *w : std::as_const( mFormWidgets ) )
|
|
{
|
|
QString filter = w->currentFilterExpression();
|
|
if ( !filter.isEmpty() )
|
|
filters << filter;
|
|
}
|
|
|
|
if ( filters.isEmpty() )
|
|
return QString();
|
|
|
|
QString filter = filters.join( QLatin1String( ") AND (" ) ).prepend( '(' ).append( ')' );
|
|
return filter;
|
|
}
|
|
|
|
QgsExpressionContext QgsAttributeForm::createExpressionContext( const QgsFeature &feature ) const
|
|
{
|
|
QgsExpressionContext context;
|
|
context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );
|
|
context.appendScope( QgsExpressionContextUtils::formScope( feature, mContext.attributeFormModeString() ) );
|
|
if ( mExtraContextScope )
|
|
{
|
|
context.appendScope( new QgsExpressionContextScope( *mExtraContextScope.get() ) );
|
|
}
|
|
if ( mContext.parentFormFeature().isValid() )
|
|
{
|
|
context.appendScope( QgsExpressionContextUtils::parentFormScope( mContext.parentFormFeature() ) );
|
|
}
|
|
context.setFeature( feature );
|
|
return context;
|
|
}
|
|
|
|
|
|
void QgsAttributeForm::onAttributeChanged( const QVariant &value, const QVariantList &additionalFieldValues )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
|
|
Q_ASSERT( eww );
|
|
|
|
bool signalEmitted = false;
|
|
|
|
if ( mValuesInitialized )
|
|
mDirty = true;
|
|
|
|
mCurrentFormFeature.setAttribute( eww->field().name(), value );
|
|
|
|
switch ( mMode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
{
|
|
Q_NOWARN_DEPRECATED_PUSH
|
|
emit attributeChanged( eww->field().name(), value );
|
|
Q_NOWARN_DEPRECATED_POP
|
|
emit widgetValueChanged( eww->field().name(), value, !mIsSettingFeature );
|
|
|
|
// also emit the signal for additional values
|
|
const QStringList additionalFields = eww->additionalFields();
|
|
for ( int i = 0; i < additionalFields.count(); i++ )
|
|
{
|
|
const QString fieldName = additionalFields.at( i );
|
|
const QVariant value = additionalFieldValues.at( i );
|
|
emit widgetValueChanged( fieldName, value, !mIsSettingFeature );
|
|
}
|
|
|
|
signalEmitted = true;
|
|
|
|
if ( mValuesInitialized )
|
|
updateJoinedFields( *eww );
|
|
|
|
break;
|
|
}
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
{
|
|
if ( !mIsSettingMultiEditFeatures )
|
|
{
|
|
mUnsavedMultiEditChanges = true;
|
|
|
|
QLabel *msgLabel = new QLabel( tr( "Unsaved multiedit changes: <a href=\"#apply\">apply changes</a> or <a href=\"#reset\">reset changes</a>." ), mMessageBar );
|
|
msgLabel->setAlignment( Qt::AlignLeft | Qt::AlignVCenter );
|
|
msgLabel->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
|
|
connect( msgLabel, &QLabel::linkActivated, this, &QgsAttributeForm::multiEditMessageClicked );
|
|
clearMultiEditMessages();
|
|
|
|
mMultiEditUnsavedMessageBarItem = new QgsMessageBarItem( msgLabel, Qgis::MessageLevel::Warning );
|
|
if ( !mButtonBox->isVisible() )
|
|
mMessageBar->pushItem( mMultiEditUnsavedMessageBarItem );
|
|
|
|
emit widgetValueChanged( eww->field().name(), value, false );
|
|
signalEmitted = true;
|
|
}
|
|
break;
|
|
}
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
//nothing to do
|
|
break;
|
|
}
|
|
|
|
// Update other widgets pointing to the same field, required to happen now to insure
|
|
// currentFormValuesFeature() gets the right value when processing constraints
|
|
const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
|
|
for ( QgsAttributeFormEditorWidget *formEditorWidget : formEditorWidgets )
|
|
{
|
|
if ( formEditorWidget->editorWidget() == eww )
|
|
continue;
|
|
|
|
// formEditorWidget and eww points to the same field, so block signals
|
|
// as there is no need to handle valueChanged again for each duplicate
|
|
whileBlocking( formEditorWidget->editorWidget() )->setValue( value );
|
|
}
|
|
|
|
updateConstraints( eww );
|
|
|
|
// Update dependent fields (only if form is not initializing)
|
|
if ( mValuesInitialized )
|
|
{
|
|
//append field index here, so it's not updated recursive
|
|
mAlreadyUpdatedFields.append( eww->fieldIdx() );
|
|
updateValuesDependencies( eww->fieldIdx() );
|
|
mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );
|
|
}
|
|
|
|
// Updates expression controlled labels and editable state
|
|
updateLabels();
|
|
updateEditableState();
|
|
|
|
if ( !signalEmitted )
|
|
{
|
|
Q_NOWARN_DEPRECATED_PUSH
|
|
emit attributeChanged( eww->field().name(), value );
|
|
Q_NOWARN_DEPRECATED_POP
|
|
bool attributeHasChanged = !mIsSettingFeature;
|
|
if ( mMode == QgsAttributeEditorContext::MultiEditMode )
|
|
attributeHasChanged &= !mIsSettingMultiEditFeatures;
|
|
|
|
emit widgetValueChanged( eww->field().name(), value, attributeHasChanged );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateAllConstraints()
|
|
{
|
|
const auto constMWidgets = mWidgets;
|
|
for ( QgsWidgetWrapper *ww : constMWidgets )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
updateConstraints( eww );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateConstraints( QgsEditorWidgetWrapper *eww )
|
|
{
|
|
// get the current feature set in the form
|
|
QgsFeature ft;
|
|
if ( currentFormValuesFeature( ft ) )
|
|
{
|
|
// if the layer is NOT being edited then we only check layer based constraints, and not
|
|
// any constraints enforced by the provider. Because:
|
|
// 1. we want to keep browsing features nice and responsive. It's nice to give feedback as to whether
|
|
// the value checks out, but not if it's too slow to do so. Some constraints (e.g., unique) can be
|
|
// expensive to test. A user can freely remove a layer-based constraint if it proves to be too slow
|
|
// to test, but they are unlikely to have any control over provider-side constraints
|
|
// 2. the provider has already accepted the value, so presumably it doesn't violate the constraint
|
|
// and there's no point rechecking!
|
|
|
|
// update eww constraint
|
|
updateConstraint( ft, eww );
|
|
|
|
// update eww dependencies constraint
|
|
const QList<QgsEditorWidgetWrapper *> deps = constraintDependencies( eww );
|
|
|
|
for ( QgsEditorWidgetWrapper *depsEww : deps )
|
|
updateConstraint( ft, depsEww );
|
|
|
|
// sync OK button status
|
|
synchronizeState();
|
|
|
|
QgsExpressionContext context = createExpressionContext( ft );
|
|
|
|
// Recheck visibility/collapsed state for all containers which are controlled by this value
|
|
const QVector<ContainerInformation *> infos = mContainerInformationDependency.value( eww->field().name() );
|
|
for ( ContainerInformation *info : infos )
|
|
{
|
|
info->apply( &context );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateContainersVisibility()
|
|
{
|
|
QgsExpressionContext context = createExpressionContext( mFeature );
|
|
|
|
const QVector<ContainerInformation *> infos = mContainerVisibilityCollapsedInformation;
|
|
|
|
for ( ContainerInformation *info : infos )
|
|
{
|
|
info->apply( &context );
|
|
}
|
|
|
|
// Update the constraints if not in multi edit, because
|
|
// when mode changes to multi edit, constraints have been already
|
|
// updated and a further update will use current form feature values,
|
|
// possibly empty for mixed values, leading to false positive
|
|
// constraints violations.
|
|
if ( mMode != QgsAttributeEditorContext::Mode::MultiEditMode )
|
|
{
|
|
updateAllConstraints();
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateConstraint( const QgsFeature &ft, QgsEditorWidgetWrapper *eww )
|
|
{
|
|
|
|
QgsFieldConstraints::ConstraintOrigin constraintOrigin = mLayer->isEditable() ? QgsFieldConstraints::ConstraintOriginNotSet : QgsFieldConstraints::ConstraintOriginLayer;
|
|
|
|
if ( eww->layer()->fields().fieldOrigin( eww->fieldIdx() ) == Qgis::FieldOrigin::Join )
|
|
{
|
|
int srcFieldIdx;
|
|
const QgsVectorLayerJoinInfo *info = eww->layer()->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), eww->layer()->fields(), srcFieldIdx );
|
|
|
|
if ( info && info->joinLayer() && info->isDynamicFormEnabled() )
|
|
{
|
|
if ( mJoinedFeatures.contains( info ) )
|
|
{
|
|
eww->updateConstraint( info->joinLayer(), srcFieldIdx, mJoinedFeatures[info], constraintOrigin );
|
|
return;
|
|
}
|
|
else // if we are here, it means there's not joined field for this feature
|
|
{
|
|
eww->updateConstraint( QgsFeature() );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// default constraint update
|
|
eww->updateConstraint( ft, constraintOrigin );
|
|
|
|
}
|
|
|
|
void QgsAttributeForm::updateLabels()
|
|
{
|
|
if ( ! mLabelDataDefinedProperties.isEmpty() )
|
|
{
|
|
QgsFeature currentFeature;
|
|
if ( currentFormValuesFeature( currentFeature ) )
|
|
{
|
|
QgsExpressionContext context = createExpressionContext( currentFeature );
|
|
|
|
for ( auto it = mLabelDataDefinedProperties.constBegin() ; it != mLabelDataDefinedProperties.constEnd(); ++it )
|
|
{
|
|
QLabel *label { it.key() };
|
|
bool ok;
|
|
const QString value { it->valueAsString( context, QString(), &ok ) };
|
|
if ( ok && ! value.isEmpty() )
|
|
{
|
|
label->setText( value );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateEditableState()
|
|
{
|
|
if ( ! mEditableDataDefinedProperties.isEmpty() )
|
|
{
|
|
QgsFeature currentFeature;
|
|
if ( currentFormValuesFeature( currentFeature ) )
|
|
{
|
|
QgsExpressionContext context = createExpressionContext( currentFeature );
|
|
|
|
for ( auto it = mEditableDataDefinedProperties.constBegin() ; it != mEditableDataDefinedProperties.constEnd(); ++it )
|
|
{
|
|
QWidget *w { it.key() };
|
|
bool ok;
|
|
const bool isEditable { it->valueAsBool( context, true, &ok ) && mLayer && mLayer->isEditable() }; // *NOPAD*
|
|
if ( ok )
|
|
{
|
|
QgsAttributeFormEditorWidget *editorWidget { qobject_cast<QgsAttributeFormEditorWidget *>( w ) };
|
|
if ( editorWidget )
|
|
{
|
|
editorWidget->editorWidget()->setEnabled( isEditable );
|
|
}
|
|
else
|
|
{
|
|
w->setEnabled( isEditable );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool QgsAttributeForm::currentFormValuesFeature( QgsFeature &feature )
|
|
{
|
|
bool rc = true;
|
|
feature = QgsFeature( mFeature );
|
|
QgsAttributes dst = feature.attributes();
|
|
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
|
|
if ( !eww )
|
|
continue;
|
|
|
|
if ( dst.count() > eww->fieldIdx() )
|
|
{
|
|
QVariantList dstVars = QVariantList() << dst.at( eww->fieldIdx() );
|
|
QVariantList srcVars = QVariantList() << eww->value();
|
|
QList<int> fieldIndexes = QList<int>() << eww->fieldIdx();
|
|
|
|
// append additional fields
|
|
const QStringList additionalFields = eww->additionalFields();
|
|
for ( const QString &fieldName : additionalFields )
|
|
{
|
|
int idx = eww->layer()->fields().lookupField( fieldName );
|
|
fieldIndexes << idx;
|
|
dstVars << dst.at( idx );
|
|
}
|
|
srcVars.append( eww->additionalFieldValues() );
|
|
|
|
Q_ASSERT( dstVars.count() == srcVars.count() );
|
|
|
|
for ( int i = 0; i < dstVars.count(); i++ )
|
|
{
|
|
// need to check dstVar.isNull() != srcVar.isNull()
|
|
// otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
|
|
if ( ( !qgsVariantEqual( dstVars[i], srcVars[i] ) || QgsVariantUtils::isNull( dstVars[i] ) != QgsVariantUtils::isNull( srcVars[i] ) ) && srcVars[i].isValid() )
|
|
{
|
|
dst[fieldIndexes[i]] = srcVars[i];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rc = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
feature.setAttributes( dst );
|
|
|
|
return rc;
|
|
}
|
|
|
|
|
|
void QgsAttributeForm::registerContainerInformation( QgsAttributeForm::ContainerInformation *info )
|
|
{
|
|
mContainerVisibilityCollapsedInformation.append( info );
|
|
|
|
const QSet<QString> referencedColumns = info->expression.referencedColumns().unite( info->collapsedExpression.referencedColumns() );
|
|
|
|
for ( const QString &col : referencedColumns )
|
|
{
|
|
mContainerInformationDependency[ col ].append( info );
|
|
}
|
|
}
|
|
|
|
bool QgsAttributeForm::currentFormValidConstraints( QStringList &invalidFields, QStringList &descriptions ) const
|
|
{
|
|
bool valid{ true };
|
|
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
if ( ! eww->isValidConstraint() )
|
|
{
|
|
invalidFields.append( eww->field().displayName() );
|
|
|
|
descriptions.append( eww->constraintFailureReason() );
|
|
|
|
if ( eww->isBlockingCommit() )
|
|
valid = false; // continue to get all invalid fields
|
|
}
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
bool QgsAttributeForm::currentFormValidHardConstraints( QStringList &invalidFields, QStringList &descriptions ) const
|
|
{
|
|
bool valid{ true };
|
|
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
if ( eww->isBlockingCommit() )
|
|
{
|
|
invalidFields.append( eww->field().displayName() );
|
|
descriptions.append( eww->constraintFailureReason() );
|
|
valid = false; // continue to get all invalid fields
|
|
}
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
void QgsAttributeForm::onAttributeAdded( int idx )
|
|
{
|
|
mPreventFeatureRefresh = false;
|
|
if ( mFeature.isValid() )
|
|
{
|
|
QgsAttributes attrs = mFeature.attributes();
|
|
attrs.insert( idx, QgsVariantUtils::createNullVariant( layer()->fields().at( idx ).type() ) );
|
|
mFeature.setFields( layer()->fields() );
|
|
mFeature.setAttributes( attrs );
|
|
}
|
|
init();
|
|
setFeature( mFeature );
|
|
}
|
|
|
|
void QgsAttributeForm::onAttributeDeleted( int idx )
|
|
{
|
|
mPreventFeatureRefresh = false;
|
|
if ( mFeature.isValid() )
|
|
{
|
|
QgsAttributes attrs = mFeature.attributes();
|
|
attrs.remove( idx );
|
|
mFeature.setFields( layer()->fields() );
|
|
mFeature.setAttributes( attrs );
|
|
}
|
|
init();
|
|
setFeature( mFeature );
|
|
}
|
|
|
|
void QgsAttributeForm::onRelatedFeaturesChanged()
|
|
{
|
|
updateRelatedLayerFields();
|
|
}
|
|
|
|
void QgsAttributeForm::onUpdatedFields()
|
|
{
|
|
mPreventFeatureRefresh = false;
|
|
if ( mFeature.isValid() )
|
|
{
|
|
QgsAttributes attrs( layer()->fields().size() );
|
|
for ( int i = 0; i < layer()->fields().size(); i++ )
|
|
{
|
|
int idx = mFeature.fields().indexFromName( layer()->fields().at( i ).name() );
|
|
if ( idx != -1 )
|
|
{
|
|
attrs[i] = mFeature.attributes().at( idx );
|
|
if ( mFeature.attributes().at( idx ).userType() != layer()->fields().at( i ).type() )
|
|
{
|
|
attrs[i].convert( layer()->fields().at( i ).type() );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
attrs[i] = QgsVariantUtils::createNullVariant( layer()->fields().at( i ).type() );
|
|
}
|
|
}
|
|
mFeature.setFields( layer()->fields() );
|
|
mFeature.setAttributes( attrs );
|
|
}
|
|
init();
|
|
setFeature( mFeature );
|
|
}
|
|
|
|
void QgsAttributeForm::onConstraintStatusChanged( const QString &constraint,
|
|
const QString &description, const QString &err, QgsEditorWidgetWrapper::ConstraintResult result )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
|
|
Q_ASSERT( eww );
|
|
|
|
const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
|
|
|
|
for ( QgsAttributeFormEditorWidget *formEditorWidget : formEditorWidgets )
|
|
{
|
|
formEditorWidget->setConstraintStatus( constraint, description, err, result );
|
|
if ( formEditorWidget->editorWidget() != eww )
|
|
{
|
|
formEditorWidget->editorWidget()->updateConstraint( result, err );
|
|
}
|
|
}
|
|
}
|
|
|
|
QList<QgsEditorWidgetWrapper *> QgsAttributeForm::constraintDependencies( QgsEditorWidgetWrapper *w )
|
|
{
|
|
QList<QgsEditorWidgetWrapper *> wDeps;
|
|
QString name = w->field().name();
|
|
|
|
// for each widget in the current form
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
// get the wrapper
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
// compare name to not compare w to itself
|
|
QString ewwName = eww->field().name();
|
|
if ( name != ewwName )
|
|
{
|
|
// get expression and referencedColumns
|
|
QgsExpression expr = eww->layer()->fields().at( eww->fieldIdx() ).constraints().constraintExpression();
|
|
|
|
const auto referencedColumns = expr.referencedColumns();
|
|
|
|
for ( const QString &colName : referencedColumns )
|
|
{
|
|
if ( name == colName )
|
|
{
|
|
wDeps.append( eww );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return wDeps;
|
|
}
|
|
|
|
QgsRelationWidgetWrapper *QgsAttributeForm::setupRelationWidgetWrapper( const QgsRelation &rel, const QgsAttributeEditorContext &context )
|
|
{
|
|
return setupRelationWidgetWrapper( QString(), rel, context );
|
|
}
|
|
|
|
QgsRelationWidgetWrapper *QgsAttributeForm::setupRelationWidgetWrapper( const QString &relationWidgetTypeId, const QgsRelation &rel, const QgsAttributeEditorContext &context )
|
|
{
|
|
QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( relationWidgetTypeId, mLayer, rel, nullptr, this );
|
|
const QVariantMap config = mLayer->editFormConfig().widgetConfig( rel.id() );
|
|
rww->setConfig( config );
|
|
rww->setContext( context );
|
|
|
|
return rww;
|
|
}
|
|
|
|
void QgsAttributeForm::preventFeatureRefresh()
|
|
{
|
|
mPreventFeatureRefresh = true;
|
|
}
|
|
|
|
void QgsAttributeForm::refreshFeature()
|
|
{
|
|
if ( mPreventFeatureRefresh || mLayer->isEditable() || !mFeature.isValid() )
|
|
return;
|
|
|
|
// reload feature if layer changed although not editable
|
|
// (datasource probably changed bypassing QgsVectorLayer)
|
|
if ( !mLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeature.id() ) ).nextFeature( mFeature ) )
|
|
return;
|
|
|
|
init();
|
|
setFeature( mFeature );
|
|
}
|
|
|
|
void QgsAttributeForm::parentFormValueChanged( const QString &attribute, const QVariant &newValue )
|
|
{
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
eww->parentFormValueChanged( attribute, newValue );
|
|
}
|
|
}
|
|
}
|
|
|
|
bool QgsAttributeForm::needsGeometry() const
|
|
{
|
|
return mNeedsGeometry;
|
|
}
|
|
|
|
void QgsAttributeForm::synchronizeState()
|
|
{
|
|
bool isEditable = ( mFeature.isValid()
|
|
|| mMode == QgsAttributeEditorContext::AddFeatureMode
|
|
|| mMode == QgsAttributeEditorContext::MultiEditMode ) && mLayer->isEditable();
|
|
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( eww )
|
|
{
|
|
const QList<QgsAttributeFormEditorWidget *> formWidgets = mFormEditorWidgets.values( eww->fieldIdx() );
|
|
|
|
for ( QgsAttributeFormEditorWidget *formWidget : formWidgets )
|
|
formWidget->setConstraintResultVisible( isEditable );
|
|
|
|
eww->setConstraintResultVisible( isEditable );
|
|
|
|
bool enabled = isEditable && fieldIsEditable( eww->fieldIdx() );
|
|
ww->setEnabled( enabled );
|
|
|
|
updateIcon( eww );
|
|
}
|
|
else // handle QgsWidgetWrapper different than QgsEditorWidgetWrapper
|
|
{
|
|
ww->setEnabled( isEditable );
|
|
}
|
|
|
|
}
|
|
|
|
|
|
if ( mMode != QgsAttributeEditorContext::SearchMode )
|
|
{
|
|
if ( mMode == QgsAttributeEditorContext::Mode::MultiEditMode && mLayer->selectedFeatureCount() == 0 )
|
|
{
|
|
isEditable = false;
|
|
if ( mConstraintsFailMessageBarItem )
|
|
{
|
|
mMessageBar->popWidget( mConstraintsFailMessageBarItem );
|
|
}
|
|
mConstraintsFailMessageBarItem = new QgsMessageBarItem( tr( "Multi edit mode requires at least one selected feature." ), Qgis::MessageLevel::Info, -1 );
|
|
mMessageBar->pushItem( mConstraintsFailMessageBarItem );
|
|
}
|
|
else
|
|
{
|
|
QStringList invalidFields, descriptions;
|
|
mValidConstraints = currentFormValidHardConstraints( invalidFields, descriptions );
|
|
|
|
if ( isEditable && mContext.formMode() == QgsAttributeEditorContext::Embed )
|
|
{
|
|
if ( !mValidConstraints && !mConstraintsFailMessageBarItem )
|
|
{
|
|
mConstraintsFailMessageBarItem = new QgsMessageBarItem( tr( "Changes to this form will not be saved. %n field(s) don't meet their constraints.", "invalid fields", invalidFields.size() ), Qgis::MessageLevel::Warning, -1 );
|
|
mMessageBar->pushItem( mConstraintsFailMessageBarItem );
|
|
}
|
|
else if ( mValidConstraints && mConstraintsFailMessageBarItem )
|
|
{
|
|
mMessageBar->popWidget( mConstraintsFailMessageBarItem );
|
|
mConstraintsFailMessageBarItem = nullptr;
|
|
}
|
|
}
|
|
else if ( mConstraintsFailMessageBarItem )
|
|
{
|
|
mMessageBar->popWidget( mConstraintsFailMessageBarItem );
|
|
mConstraintsFailMessageBarItem = nullptr;
|
|
}
|
|
|
|
isEditable = isEditable & mValidConstraints;
|
|
}
|
|
}
|
|
|
|
// change OK button status
|
|
QPushButton *okButton = mButtonBox->button( QDialogButtonBox::Ok );
|
|
if ( okButton )
|
|
okButton->setEnabled( isEditable );
|
|
}
|
|
|
|
void QgsAttributeForm::init()
|
|
{
|
|
QApplication::setOverrideCursor( QCursor( Qt::WaitCursor ) );
|
|
|
|
// Cleanup of any previously shown widget, we start from scratch
|
|
QWidget *formWidget = nullptr;
|
|
mNeedsGeometry = false;
|
|
|
|
bool buttonBoxVisible = true;
|
|
// Cleanup button box but preserve visibility
|
|
if ( mButtonBox )
|
|
{
|
|
buttonBoxVisible = mButtonBox->isVisible();
|
|
delete mButtonBox;
|
|
mButtonBox = nullptr;
|
|
}
|
|
|
|
if ( mSearchButtonBox )
|
|
{
|
|
delete mSearchButtonBox;
|
|
mSearchButtonBox = nullptr;
|
|
}
|
|
|
|
qDeleteAll( mWidgets );
|
|
mWidgets.clear();
|
|
|
|
while ( QWidget *w = this->findChild<QWidget *>() )
|
|
{
|
|
delete w;
|
|
}
|
|
delete layout();
|
|
|
|
QVBoxLayout *vl = new QVBoxLayout();
|
|
vl->setContentsMargins( 0, 0, 0, 0 );
|
|
mMessageBar = new QgsMessageBar( this );
|
|
mMessageBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
|
|
vl->addWidget( mMessageBar );
|
|
|
|
setLayout( vl );
|
|
|
|
// Get a layout
|
|
QGridLayout *layout = new QGridLayout();
|
|
QWidget *container = new QWidget();
|
|
container->setLayout( layout );
|
|
vl->addWidget( container );
|
|
|
|
mFormEditorWidgets.clear();
|
|
mFormWidgets.clear();
|
|
|
|
// a bar to warn the user with non-blocking messages
|
|
setContentsMargins( 0, 0, 0, 0 );
|
|
|
|
// Try to load Ui-File for layout
|
|
if ( mContext.allowCustomUi() && mLayer->editFormConfig().layout() == Qgis::AttributeFormLayout::UiFile &&
|
|
!mLayer->editFormConfig().uiForm().isEmpty() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "loading form: %1" ).arg( mLayer->editFormConfig().uiForm() ), 2 );
|
|
const QString path = mLayer->editFormConfig().uiForm();
|
|
QFile *file = QgsApplication::networkContentFetcherRegistry()->localFile( path );
|
|
if ( file && file->open( QFile::ReadOnly ) )
|
|
{
|
|
QUiLoader loader;
|
|
|
|
QFileInfo fi( file->fileName() );
|
|
loader.setWorkingDirectory( fi.dir() );
|
|
formWidget = loader.load( file, this );
|
|
if ( formWidget )
|
|
{
|
|
formWidget->setWindowFlags( Qt::Widget );
|
|
layout->addWidget( formWidget );
|
|
formWidget->show();
|
|
file->close();
|
|
mButtonBox = findChild<QDialogButtonBox *>();
|
|
createWrappers();
|
|
|
|
formWidget->installEventFilter( this );
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsTabWidget *tabWidget = nullptr;
|
|
|
|
// Tab layout
|
|
if ( !formWidget && mLayer->editFormConfig().layout() == Qgis::AttributeFormLayout::DragAndDrop )
|
|
{
|
|
int row = 0;
|
|
int column = 0;
|
|
int columnCount = 1;
|
|
bool hasRootFields = false;
|
|
bool addSpacer = true;
|
|
|
|
const QList<QgsAttributeEditorElement *> tabs = mLayer->editFormConfig().tabs();
|
|
|
|
for ( QgsAttributeEditorElement *widgDef : tabs )
|
|
{
|
|
if ( widgDef->type() == Qgis::AttributeEditorType::Container )
|
|
{
|
|
QgsAttributeEditorContainer *containerDef = dynamic_cast<QgsAttributeEditorContainer *>( widgDef );
|
|
if ( !containerDef )
|
|
continue;
|
|
|
|
switch ( containerDef->type() )
|
|
{
|
|
case Qgis::AttributeEditorContainerType::GroupBox:
|
|
{
|
|
tabWidget = nullptr;
|
|
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
|
|
if ( widgetInfo.labelStyle.overrideColor )
|
|
{
|
|
if ( widgetInfo.labelStyle.color.isValid() )
|
|
{
|
|
widgetInfo.widget->setStyleSheet( QStringLiteral( "QGroupBox::title { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
|
|
}
|
|
}
|
|
if ( widgetInfo.labelStyle.overrideFont )
|
|
{
|
|
widgetInfo.widget->setFont( widgetInfo.labelStyle.font );
|
|
}
|
|
|
|
layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
|
|
if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
|
|
{
|
|
layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
|
|
}
|
|
if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
|
|
{
|
|
layout->setRowStretch( row, widgDef->verticalStretch() );
|
|
addSpacer = false;
|
|
}
|
|
|
|
if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
|
|
{
|
|
registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(), containerDef->collapsed(), containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression() ) );
|
|
}
|
|
column += 2;
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorContainerType::Row:
|
|
{
|
|
tabWidget = nullptr;
|
|
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
|
|
layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
|
|
if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
|
|
{
|
|
layout->setRowStretch( row, widgDef->verticalStretch() );
|
|
addSpacer = false;
|
|
}
|
|
if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
|
|
{
|
|
layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
|
|
}
|
|
|
|
if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
|
|
{
|
|
registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(), containerDef->collapsed(), containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression() ) );
|
|
}
|
|
column += 2;
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorContainerType::Tab:
|
|
{
|
|
if ( !tabWidget )
|
|
{
|
|
tabWidget = new QgsTabWidget();
|
|
layout->addWidget( tabWidget, row, column, 1, 2 );
|
|
column += 2;
|
|
}
|
|
|
|
QWidget *tabPage = new QWidget( tabWidget );
|
|
|
|
tabWidget->addTab( tabPage, widgDef->name() );
|
|
tabWidget->setTabStyle( tabWidget->tabBar()->count() - 1, widgDef->labelStyle() );
|
|
|
|
if ( containerDef->visibilityExpression().enabled() )
|
|
{
|
|
registerContainerInformation( new ContainerInformation( tabWidget, tabPage, containerDef->visibilityExpression().data() ) );
|
|
}
|
|
QGridLayout *tabPageLayout = new QGridLayout();
|
|
tabPage->setLayout( tabPageLayout );
|
|
|
|
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, tabPage, mLayer, mContext );
|
|
tabPageLayout->addWidget( widgetInfo.widget );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if ( widgDef->type() == Qgis::AttributeEditorType::Relation )
|
|
{
|
|
hasRootFields = true;
|
|
tabWidget = nullptr;
|
|
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
|
|
QgsCollapsibleGroupBox *collapsibleGroupBox = new QgsCollapsibleGroupBox();
|
|
|
|
if ( widgetInfo.showLabel )
|
|
{
|
|
if ( widgetInfo.labelStyle.overrideColor && widgetInfo.labelStyle.color.isValid() )
|
|
{
|
|
collapsibleGroupBox->setStyleSheet( QStringLiteral( "QGroupBox::title { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
|
|
}
|
|
|
|
if ( widgetInfo.labelStyle.overrideFont )
|
|
{
|
|
collapsibleGroupBox->setFont( widgetInfo.labelStyle.font );
|
|
}
|
|
|
|
collapsibleGroupBox->setTitle( widgetInfo.labelText );
|
|
}
|
|
|
|
QVBoxLayout *collapsibleGroupBoxLayout = new QVBoxLayout();
|
|
collapsibleGroupBoxLayout->addWidget( widgetInfo.widget );
|
|
collapsibleGroupBox->setLayout( collapsibleGroupBoxLayout );
|
|
|
|
QVBoxLayout *c = new QVBoxLayout();
|
|
c->addWidget( collapsibleGroupBox );
|
|
layout->addLayout( c, row, column, 1, 2 );
|
|
|
|
if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
|
|
layout->setRowStretch( row, widgDef->verticalStretch() );
|
|
if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
|
|
layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
|
|
|
|
column += 2;
|
|
|
|
// we consider all relation editors should be expanding
|
|
addSpacer = false;
|
|
}
|
|
else
|
|
{
|
|
hasRootFields = true;
|
|
tabWidget = nullptr;
|
|
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
|
|
QLabel *label = new QLabel( widgetInfo.labelText );
|
|
|
|
if ( widgetInfo.labelStyle.overrideColor )
|
|
{
|
|
if ( widgetInfo.labelStyle.color.isValid() )
|
|
{
|
|
label->setStyleSheet( QStringLiteral( "QLabel { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
|
|
}
|
|
}
|
|
|
|
if ( widgetInfo.labelStyle.overrideFont )
|
|
{
|
|
label->setFont( widgetInfo.labelStyle.font );
|
|
}
|
|
|
|
label->setToolTip( widgetInfo.toolTip );
|
|
if ( columnCount > 1 && !widgetInfo.labelOnTop )
|
|
{
|
|
label->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
|
|
}
|
|
|
|
label->setBuddy( widgetInfo.widget );
|
|
|
|
// If at least one expanding widget is present do not add a spacer
|
|
if ( widgetInfo.widget
|
|
&& widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Fixed
|
|
&& widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Maximum
|
|
&& widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Preferred )
|
|
addSpacer = false;
|
|
|
|
if ( !widgetInfo.showLabel )
|
|
{
|
|
QVBoxLayout *c = new QVBoxLayout();
|
|
label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
|
|
c->addWidget( widgetInfo.widget );
|
|
layout->addLayout( c, row, column, 1, 2 );
|
|
|
|
if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
|
|
{
|
|
layout->setRowStretch( row, widgDef->verticalStretch() );
|
|
addSpacer = false;
|
|
}
|
|
if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
|
|
{
|
|
layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
|
|
}
|
|
|
|
column += 2;
|
|
}
|
|
else if ( widgetInfo.labelOnTop )
|
|
{
|
|
QVBoxLayout *c = new QVBoxLayout();
|
|
label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
|
|
c->addWidget( label );
|
|
c->addWidget( widgetInfo.widget );
|
|
layout->addLayout( c, row, column, 1, 2 );
|
|
|
|
if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
|
|
{
|
|
layout->setRowStretch( row, widgDef->verticalStretch() );
|
|
addSpacer = false;
|
|
}
|
|
if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( column + 1 ) )
|
|
{
|
|
layout->setColumnStretch( column + 1, widgDef->horizontalStretch() );
|
|
}
|
|
|
|
column += 2;
|
|
}
|
|
else
|
|
{
|
|
const int widgetColumn = column + 1;
|
|
layout->addWidget( label, row, column++ );
|
|
layout->addWidget( widgetInfo.widget, row, column++ );
|
|
|
|
if ( widgDef->verticalStretch() > 0 && widgDef->verticalStretch() > layout->rowStretch( row ) )
|
|
{
|
|
layout->setRowStretch( row, widgDef->verticalStretch() );
|
|
addSpacer = false;
|
|
}
|
|
if ( widgDef->horizontalStretch() > 0 && widgDef->horizontalStretch() > layout->columnStretch( widgetColumn ) )
|
|
{
|
|
layout->setColumnStretch( widgetColumn, widgDef->horizontalStretch() );
|
|
}
|
|
}
|
|
|
|
// Alias DD overrides
|
|
if ( widgDef->type() == Qgis::AttributeEditorType::Field )
|
|
{
|
|
const QgsAttributeEditorField *fieldElement { static_cast<QgsAttributeEditorField *>( widgDef ) };
|
|
const int fieldIdx = fieldElement->idx();
|
|
if ( fieldIdx >= 0 && fieldIdx < mLayer->fields().count() )
|
|
{
|
|
const QString fieldName { mLayer->fields().at( fieldIdx ).name() };
|
|
if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Alias ) )
|
|
{
|
|
const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Alias ) };
|
|
if ( property.isActive() )
|
|
{
|
|
mLabelDataDefinedProperties[ label ] = property;
|
|
}
|
|
}
|
|
if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Editable ) )
|
|
{
|
|
const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Editable ) };
|
|
if ( property.isActive() )
|
|
{
|
|
mEditableDataDefinedProperties[ widgetInfo.widget ] = property;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( column >= columnCount * 2 )
|
|
{
|
|
column = 0;
|
|
row += 1;
|
|
}
|
|
}
|
|
|
|
if ( hasRootFields && addSpacer )
|
|
{
|
|
QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding );
|
|
layout->addItem( spacerItem, row, 0 );
|
|
layout->setRowStretch( row, 1 );
|
|
}
|
|
|
|
formWidget = container;
|
|
}
|
|
|
|
// Autogenerate Layout
|
|
// If there is still no layout loaded (defined as autogenerate or other methods failed)
|
|
mIconMap.clear();
|
|
|
|
if ( !formWidget )
|
|
{
|
|
formWidget = new QWidget( this );
|
|
QGridLayout *gridLayout = new QGridLayout( formWidget );
|
|
formWidget->setLayout( gridLayout );
|
|
|
|
if ( mContext.formMode() != QgsAttributeEditorContext::Embed )
|
|
{
|
|
// put the form into a scroll area to nicely handle cases with lots of attributes
|
|
QgsScrollArea *scrollArea = new QgsScrollArea( this );
|
|
scrollArea->setWidget( formWidget );
|
|
scrollArea->setWidgetResizable( true );
|
|
scrollArea->setFrameShape( QFrame::NoFrame );
|
|
scrollArea->setFrameShadow( QFrame::Plain );
|
|
scrollArea->setFocusProxy( this );
|
|
layout->addWidget( scrollArea );
|
|
}
|
|
else
|
|
{
|
|
layout->addWidget( formWidget );
|
|
}
|
|
|
|
int row = 0;
|
|
|
|
const QgsFields fields = mLayer->fields();
|
|
|
|
for ( const QgsField &field : fields )
|
|
{
|
|
int idx = fields.lookupField( field.name() );
|
|
if ( idx < 0 )
|
|
continue;
|
|
|
|
//show attribute alias if available
|
|
QString fieldName = mLayer->attributeDisplayName( idx );
|
|
QString labelText = fieldName;
|
|
labelText.replace( '&', QLatin1String( "&&" ) ); // need to escape '&' or they'll be replace by _ in the label text
|
|
|
|
const QgsEditorWidgetSetup widgetSetup = QgsGui::editorWidgetRegistry()->findBest( mLayer, field.name() );
|
|
|
|
if ( widgetSetup.type() == QLatin1String( "Hidden" ) )
|
|
continue;
|
|
|
|
bool labelOnTop = mLayer->editFormConfig().labelOnTop( idx );
|
|
|
|
// This will also create the widget
|
|
QLabel *label = new QLabel( labelText );
|
|
label->setToolTip( QgsFieldModel::fieldToolTipExtended( field, mLayer ) );
|
|
QSvgWidget *i = new QSvgWidget();
|
|
i->setFixedSize( 18, 18 );
|
|
|
|
if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Alias ) )
|
|
{
|
|
const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Alias ) };
|
|
if ( property.isActive() )
|
|
{
|
|
mLabelDataDefinedProperties[ label ] = property;
|
|
}
|
|
}
|
|
|
|
QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( widgetSetup.type(), mLayer, idx, widgetSetup.config(), nullptr, this, mContext );
|
|
|
|
QWidget *w = nullptr;
|
|
if ( eww )
|
|
{
|
|
QgsAttributeFormEditorWidget *formWidget = new QgsAttributeFormEditorWidget( eww, widgetSetup.type(), this );
|
|
w = formWidget;
|
|
mFormEditorWidgets.insert( idx, formWidget );
|
|
mFormWidgets.append( formWidget );
|
|
formWidget->createSearchWidgetWrappers( mContext );
|
|
|
|
label->setBuddy( eww->widget() );
|
|
|
|
if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Editable ) )
|
|
{
|
|
const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Editable ) };
|
|
if ( property.isActive() )
|
|
{
|
|
mEditableDataDefinedProperties[ formWidget ] = property;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
w = new QLabel( QStringLiteral( "<p style=\"color: red; font-style: italic;\">%1</p>" ).arg( tr( "Failed to create widget with type '%1'" ).arg( widgetSetup.type() ) ) );
|
|
}
|
|
|
|
|
|
if ( w )
|
|
w->setObjectName( field.name() );
|
|
|
|
if ( eww )
|
|
{
|
|
mWidgets.append( eww );
|
|
mIconMap[eww->widget()] = i;
|
|
}
|
|
|
|
if ( labelOnTop )
|
|
{
|
|
gridLayout->addWidget( label, row++, 0, 1, 2 );
|
|
gridLayout->addWidget( w, row++, 0, 1, 2 );
|
|
gridLayout->addWidget( i, row++, 0, 1, 2 );
|
|
}
|
|
else
|
|
{
|
|
gridLayout->addWidget( label, row, 0 );
|
|
gridLayout->addWidget( w, row, 1 );
|
|
gridLayout->addWidget( i, row++, 2 );
|
|
}
|
|
|
|
}
|
|
|
|
const QList<QgsRelation> relations = QgsProject::instance()->relationManager()->referencedRelations( mLayer );
|
|
for ( const QgsRelation &rel : relations )
|
|
{
|
|
QgsRelationWidgetWrapper *rww = setupRelationWidgetWrapper( QStringLiteral( "relation_editor" ), rel, mContext );
|
|
|
|
QgsAttributeFormRelationEditorWidget *formWidget = new QgsAttributeFormRelationEditorWidget( rww, this );
|
|
formWidget->createSearchWidgetWrappers( mContext );
|
|
|
|
QgsCollapsibleGroupBox *collapsibleGroupBox = new QgsCollapsibleGroupBox( rel.name() );
|
|
QVBoxLayout *collapsibleGroupBoxLayout = new QVBoxLayout();
|
|
collapsibleGroupBoxLayout->addWidget( formWidget );
|
|
collapsibleGroupBox->setLayout( collapsibleGroupBoxLayout );
|
|
|
|
gridLayout->addWidget( collapsibleGroupBox, row++, 0, 1, 2 );
|
|
|
|
mWidgets.append( rww );
|
|
mFormWidgets.append( formWidget );
|
|
}
|
|
|
|
if ( QgsProject::instance()->relationManager()->referencedRelations( mLayer ).isEmpty() )
|
|
{
|
|
QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding );
|
|
gridLayout->addItem( spacerItem, row, 0 );
|
|
gridLayout->setRowStretch( row, 1 );
|
|
row++;
|
|
}
|
|
}
|
|
|
|
// Prepare value dependencies
|
|
updateFieldDependencies();
|
|
|
|
if ( !mButtonBox )
|
|
{
|
|
mButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
|
|
mButtonBox->setObjectName( QStringLiteral( "buttonBox" ) );
|
|
layout->addWidget( mButtonBox, layout->rowCount(), 0, 1, layout->columnCount() );
|
|
}
|
|
mButtonBox->setVisible( buttonBoxVisible );
|
|
|
|
if ( !mSearchButtonBox )
|
|
{
|
|
mSearchButtonBox = new QWidget();
|
|
QHBoxLayout *boxLayout = new QHBoxLayout();
|
|
boxLayout->setContentsMargins( 0, 0, 0, 0 );
|
|
mSearchButtonBox->setLayout( boxLayout );
|
|
mSearchButtonBox->setObjectName( QStringLiteral( "searchButtonBox" ) );
|
|
|
|
QPushButton *clearButton = new QPushButton( tr( "&Reset Form" ), mSearchButtonBox );
|
|
connect( clearButton, &QPushButton::clicked, this, &QgsAttributeForm::resetSearch );
|
|
boxLayout->addWidget( clearButton );
|
|
boxLayout->addStretch( 1 );
|
|
|
|
QPushButton *flashButton = new QPushButton();
|
|
flashButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
|
|
flashButton->setText( tr( "&Flash Features" ) );
|
|
connect( flashButton, &QToolButton::clicked, this, &QgsAttributeForm::searchFlash );
|
|
boxLayout->addWidget( flashButton );
|
|
|
|
QPushButton *openAttributeTableButton = new QPushButton();
|
|
openAttributeTableButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
|
|
openAttributeTableButton->setText( tr( "Show in &Table" ) );
|
|
openAttributeTableButton->setToolTip( tr( "Open the attribute table editor with the filtered features" ) );
|
|
connect( openAttributeTableButton, &QToolButton::clicked, this, [ = ]
|
|
{
|
|
emit openFilteredFeaturesAttributeTable( createFilterExpression() );
|
|
} );
|
|
boxLayout->addWidget( openAttributeTableButton );
|
|
|
|
QPushButton *zoomButton = new QPushButton();
|
|
zoomButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
|
|
zoomButton->setText( tr( "&Zoom to Features" ) );
|
|
connect( zoomButton, &QToolButton::clicked, this, &QgsAttributeForm::searchZoomTo );
|
|
boxLayout->addWidget( zoomButton );
|
|
|
|
QToolButton *selectButton = new QToolButton();
|
|
selectButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
|
|
selectButton->setText( tr( "&Select Features" ) );
|
|
selectButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFormSelect.svg" ) ) );
|
|
selectButton->setPopupMode( QToolButton::MenuButtonPopup );
|
|
selectButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
|
|
connect( selectButton, &QToolButton::clicked, this, &QgsAttributeForm::searchSetSelection );
|
|
QMenu *selectMenu = new QMenu( selectButton );
|
|
QAction *selectAction = new QAction( tr( "Select Features" ), selectMenu );
|
|
selectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFormSelect.svg" ) ) );
|
|
connect( selectAction, &QAction::triggered, this, &QgsAttributeForm::searchSetSelection );
|
|
selectMenu->addAction( selectAction );
|
|
QAction *addSelectAction = new QAction( tr( "Add to Current Selection" ), selectMenu );
|
|
addSelectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectAdd.svg" ) ) );
|
|
connect( addSelectAction, &QAction::triggered, this, &QgsAttributeForm::searchAddToSelection );
|
|
selectMenu->addAction( addSelectAction );
|
|
QAction *deselectAction = new QAction( tr( "Remove from Current Selection" ), selectMenu );
|
|
deselectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectRemove.svg" ) ) );
|
|
connect( deselectAction, &QAction::triggered, this, &QgsAttributeForm::searchRemoveFromSelection );
|
|
selectMenu->addAction( deselectAction );
|
|
QAction *filterSelectAction = new QAction( tr( "Filter Current Selection" ), selectMenu );
|
|
filterSelectAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectIntersect.svg" ) ) );
|
|
connect( filterSelectAction, &QAction::triggered, this, &QgsAttributeForm::searchIntersectSelection );
|
|
selectMenu->addAction( filterSelectAction );
|
|
selectButton->setMenu( selectMenu );
|
|
boxLayout->addWidget( selectButton );
|
|
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
|
|
{
|
|
QToolButton *filterButton = new QToolButton();
|
|
filterButton->setText( tr( "Filter Features" ) );
|
|
filterButton->setPopupMode( QToolButton::MenuButtonPopup );
|
|
filterButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
|
|
connect( filterButton, &QToolButton::clicked, this, &QgsAttributeForm::filterTriggered );
|
|
QMenu *filterMenu = new QMenu( filterButton );
|
|
QAction *filterAndAction = new QAction( tr( "Filter Within (\"AND\")" ), filterMenu );
|
|
connect( filterAndAction, &QAction::triggered, this, &QgsAttributeForm::filterAndTriggered );
|
|
filterMenu->addAction( filterAndAction );
|
|
QAction *filterOrAction = new QAction( tr( "Extend Filter (\"OR\")" ), filterMenu );
|
|
connect( filterOrAction, &QAction::triggered, this, &QgsAttributeForm::filterOrTriggered );
|
|
filterMenu->addAction( filterOrAction );
|
|
filterButton->setMenu( filterMenu );
|
|
boxLayout->addWidget( filterButton );
|
|
}
|
|
else
|
|
{
|
|
QPushButton *closeButton = new QPushButton( tr( "Close" ), mSearchButtonBox );
|
|
connect( closeButton, &QPushButton::clicked, this, &QgsAttributeForm::closed );
|
|
closeButton->setShortcut( Qt::Key_Escape );
|
|
boxLayout->addWidget( closeButton );
|
|
}
|
|
|
|
layout->addWidget( mSearchButtonBox, layout->rowCount(), 0, 1, layout->columnCount() );
|
|
}
|
|
mSearchButtonBox->setVisible( mMode == QgsAttributeEditorContext::SearchMode );
|
|
|
|
afterWidgetInit();
|
|
|
|
connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
|
|
connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
|
|
|
|
connect( mLayer, &QgsVectorLayer::editingStarted, this, &QgsAttributeForm::synchronizeState );
|
|
connect( mLayer, &QgsVectorLayer::editingStopped, this, &QgsAttributeForm::synchronizeState );
|
|
|
|
// This triggers a refresh of the form widget and gives a chance to re-format the
|
|
// value to those widgets that have a different representation when in edit mode
|
|
connect( mLayer, &QgsVectorLayer::editingStarted, this, &QgsAttributeForm::resetValues );
|
|
connect( mLayer, &QgsVectorLayer::editingStopped, this, &QgsAttributeForm::resetValues );
|
|
|
|
|
|
const auto constMInterfaces = mInterfaces;
|
|
for ( QgsAttributeFormInterface *iface : constMInterfaces )
|
|
{
|
|
iface->initForm();
|
|
}
|
|
|
|
if ( mContext.formMode() == QgsAttributeEditorContext::Embed || mMode == QgsAttributeEditorContext::SearchMode )
|
|
{
|
|
hideButtonBox();
|
|
}
|
|
|
|
QApplication::restoreOverrideCursor();
|
|
}
|
|
|
|
void QgsAttributeForm::cleanPython()
|
|
{
|
|
if ( !mPyFormVarName.isNull() )
|
|
{
|
|
QString expr = QStringLiteral( "if '%1' in locals(): del %1\n" ).arg( mPyFormVarName );
|
|
QgsPythonRunner::run( expr );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::initPython()
|
|
{
|
|
cleanPython();
|
|
|
|
// Init Python, if init function is not empty and the combo indicates
|
|
// the source for the function code
|
|
if ( !mLayer->editFormConfig().initFunction().isEmpty()
|
|
&& mLayer->editFormConfig().initCodeSource() != Qgis::AttributeFormPythonInitCodeSource::NoSource )
|
|
{
|
|
|
|
QString initFunction = mLayer->editFormConfig().initFunction();
|
|
QString initFilePath = mLayer->editFormConfig().initFilePath();
|
|
QString initCode;
|
|
|
|
switch ( mLayer->editFormConfig().initCodeSource() )
|
|
{
|
|
case Qgis::AttributeFormPythonInitCodeSource::File:
|
|
if ( !initFilePath.isEmpty() )
|
|
{
|
|
QFile *inputFile = QgsApplication::networkContentFetcherRegistry()->localFile( initFilePath );
|
|
|
|
if ( inputFile && inputFile->open( QFile::ReadOnly ) )
|
|
{
|
|
// Read it into a string
|
|
QTextStream inf( inputFile );
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
inf.setCodec( "UTF-8" );
|
|
#endif
|
|
initCode = inf.readAll();
|
|
inputFile->close();
|
|
}
|
|
else // The file couldn't be opened
|
|
{
|
|
QgsLogger::warning( QStringLiteral( "The external python file path %1 could not be opened!" ).arg( initFilePath ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsLogger::warning( QStringLiteral( "The external python file path is empty!" ) );
|
|
}
|
|
break;
|
|
|
|
case Qgis::AttributeFormPythonInitCodeSource::Dialog:
|
|
initCode = mLayer->editFormConfig().initCode();
|
|
if ( initCode.isEmpty() )
|
|
{
|
|
QgsLogger::warning( QStringLiteral( "The python code provided in the dialog is empty!" ) );
|
|
}
|
|
break;
|
|
|
|
case Qgis::AttributeFormPythonInitCodeSource::Environment:
|
|
case Qgis::AttributeFormPythonInitCodeSource::NoSource:
|
|
// Nothing to do: the function code should be already in the environment
|
|
break;
|
|
}
|
|
|
|
// If we have a function code, run it
|
|
if ( !initCode.isEmpty() )
|
|
{
|
|
if ( QgsGui::pythonMacroAllowed() )
|
|
QgsPythonRunner::run( initCode );
|
|
else
|
|
mMessageBar->pushMessage( QString(),
|
|
tr( "Python macro could not be run due to missing permissions." ),
|
|
Qgis::MessageLevel::Warning );
|
|
}
|
|
|
|
QgsPythonRunner::run( QStringLiteral( "import inspect" ) );
|
|
QString numArgs;
|
|
|
|
// Check for eval result
|
|
if ( QgsPythonRunner::eval( QStringLiteral( "len(inspect.getfullargspec(%1)[0])" ).arg( initFunction ), numArgs ) )
|
|
{
|
|
static int sFormId = 0;
|
|
mPyFormVarName = QStringLiteral( "_qgis_featureform_%1_%2" ).arg( mFormNr ).arg( sFormId++ );
|
|
|
|
QString form = QStringLiteral( "%1 = sip.wrapinstance( %2, qgis.gui.QgsAttributeForm )" )
|
|
.arg( mPyFormVarName )
|
|
.arg( ( quint64 ) this );
|
|
|
|
QgsPythonRunner::run( form );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "running featureForm init: %1" ).arg( mPyFormVarName ), 2 );
|
|
|
|
// Legacy
|
|
if ( numArgs == QLatin1String( "3" ) )
|
|
{
|
|
addInterface( new QgsAttributeFormLegacyInterface( initFunction, mPyFormVarName, this ) );
|
|
}
|
|
else
|
|
{
|
|
// If we get here, it means that the function doesn't accept three arguments
|
|
QMessageBox msgBox;
|
|
msgBox.setText( tr( "The python init function (<code>%1</code>) does not accept three arguments as expected!<br>Please check the function name in the <b>Fields</b> tab of the layer properties." ).arg( initFunction ) );
|
|
msgBox.exec();
|
|
#if 0
|
|
QString expr = QString( "%1(%2)" )
|
|
.arg( mLayer->editFormInit() )
|
|
.arg( mPyFormVarName );
|
|
QgsAttributeFormInterface *iface = QgsPythonRunner::evalToSipObject<QgsAttributeFormInterface *>( expr, "QgsAttributeFormInterface" );
|
|
if ( iface )
|
|
addInterface( iface );
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we get here, it means that inspect couldn't find the function
|
|
QMessageBox msgBox;
|
|
msgBox.setText( tr( "The python init function (<code>%1</code>) could not be found!<br>Please check the function name in the <b>Fields</b> tab of the layer properties." ).arg( initFunction ) );
|
|
msgBox.exec();
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAttributeEditorElement *widgetDef, QWidget *parent, QgsVectorLayer *vl, QgsAttributeEditorContext &context )
|
|
{
|
|
WidgetInfo newWidgetInfo;
|
|
|
|
newWidgetInfo.labelStyle = widgetDef->labelStyle();
|
|
|
|
switch ( widgetDef->type() )
|
|
{
|
|
case Qgis::AttributeEditorType::Action:
|
|
{
|
|
const QgsAttributeEditorAction *elementDef = dynamic_cast<const QgsAttributeEditorAction *>( widgetDef );
|
|
if ( !elementDef )
|
|
break;
|
|
|
|
QgsActionWidgetWrapper *actionWrapper = new QgsActionWidgetWrapper( mLayer, nullptr, this );
|
|
actionWrapper->setAction( elementDef->action( vl ) );
|
|
context.setAttributeFormMode( mMode );
|
|
actionWrapper->setContext( context );
|
|
mWidgets.append( actionWrapper );
|
|
newWidgetInfo.widget = actionWrapper->widget();
|
|
newWidgetInfo.showLabel = false;
|
|
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::Field:
|
|
{
|
|
const QgsAttributeEditorField *fieldDef = dynamic_cast<const QgsAttributeEditorField *>( widgetDef );
|
|
if ( !fieldDef )
|
|
break;
|
|
|
|
const QgsFields fields = vl->fields();
|
|
int fldIdx = fields.lookupField( fieldDef->name() );
|
|
if ( fldIdx < fields.count() && fldIdx >= 0 )
|
|
{
|
|
const QgsEditorWidgetSetup widgetSetup = QgsGui::editorWidgetRegistry()->findBest( mLayer, fieldDef->name() );
|
|
|
|
QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( widgetSetup.type(), mLayer, fldIdx, widgetSetup.config(), nullptr, this, mContext );
|
|
QgsAttributeFormEditorWidget *formWidget = new QgsAttributeFormEditorWidget( eww, widgetSetup.type(), this );
|
|
mFormEditorWidgets.insert( fldIdx, formWidget );
|
|
mFormWidgets.append( formWidget );
|
|
|
|
formWidget->createSearchWidgetWrappers( mContext );
|
|
|
|
newWidgetInfo.widget = formWidget;
|
|
mWidgets.append( eww );
|
|
|
|
newWidgetInfo.widget->setObjectName( fields.at( fldIdx ).name() );
|
|
newWidgetInfo.hint = fields.at( fldIdx ).comment();
|
|
}
|
|
|
|
newWidgetInfo.labelOnTop = mLayer->editFormConfig().labelOnTop( fldIdx );
|
|
newWidgetInfo.labelText = mLayer->attributeDisplayName( fldIdx );
|
|
newWidgetInfo.labelText.replace( '&', QLatin1String( "&&" ) ); // need to escape '&' or they'll be replace by _ in the label text
|
|
newWidgetInfo.toolTip = QStringLiteral( "<b>%1</b><p>%2</p>" ).arg( mLayer->attributeDisplayName( fldIdx ), newWidgetInfo.hint );
|
|
newWidgetInfo.showLabel = widgetDef->showLabel();
|
|
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::Relation:
|
|
{
|
|
const QgsAttributeEditorRelation *relDef = static_cast<const QgsAttributeEditorRelation *>( widgetDef );
|
|
|
|
QgsRelationWidgetWrapper *rww = setupRelationWidgetWrapper( relDef->relationWidgetTypeId(), relDef->relation(), context );
|
|
|
|
QgsAttributeFormRelationEditorWidget *formWidget = new QgsAttributeFormRelationEditorWidget( rww, this );
|
|
formWidget->createSearchWidgetWrappers( mContext );
|
|
|
|
// This needs to be after QgsAttributeFormRelationEditorWidget creation, because the widget
|
|
// does not exists yet until QgsAttributeFormRelationEditorWidget is created and the setters
|
|
// below directly alter the widget and check for it.
|
|
rww->setWidgetConfig( relDef->relationEditorConfiguration() );
|
|
rww->setNmRelationId( relDef->nmRelationId() );
|
|
rww->setForceSuppressFormPopup( relDef->forceSuppressFormPopup() );
|
|
|
|
mWidgets.append( rww );
|
|
mFormWidgets.append( formWidget );
|
|
|
|
newWidgetInfo.widget = formWidget;
|
|
newWidgetInfo.showLabel = relDef->showLabel();
|
|
newWidgetInfo.labelText = relDef->label();
|
|
if ( newWidgetInfo.labelText.isEmpty() )
|
|
newWidgetInfo.labelText = rww->relation().name();
|
|
newWidgetInfo.labelOnTop = true;
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::Container:
|
|
{
|
|
const QgsAttributeEditorContainer *container = dynamic_cast<const QgsAttributeEditorContainer *>( widgetDef );
|
|
if ( !container )
|
|
break;
|
|
|
|
int columnCount = container->columnCount();
|
|
|
|
if ( columnCount <= 0 )
|
|
columnCount = 1;
|
|
|
|
QString widgetName;
|
|
QWidget *myContainer = nullptr;
|
|
bool removeLayoutMargin = false;
|
|
switch ( container->type() )
|
|
{
|
|
case Qgis::AttributeEditorContainerType::GroupBox:
|
|
{
|
|
QgsCollapsibleGroupBoxBasic *groupBox = new QgsCollapsibleGroupBoxBasic();
|
|
widgetName = QStringLiteral( "QGroupBox" );
|
|
if ( container->showLabel() )
|
|
{
|
|
groupBox->setTitle( container->name() );
|
|
if ( newWidgetInfo.labelStyle.overrideColor )
|
|
{
|
|
if ( newWidgetInfo.labelStyle.color.isValid() )
|
|
{
|
|
groupBox->setStyleSheet( QStringLiteral( "QGroupBox::title { color: %1; }" ).arg( newWidgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
|
|
}
|
|
}
|
|
if ( newWidgetInfo.labelStyle.overrideFont )
|
|
{
|
|
groupBox->setFont( newWidgetInfo.labelStyle.font );
|
|
}
|
|
}
|
|
myContainer = groupBox;
|
|
newWidgetInfo.widget = myContainer;
|
|
groupBox->setCollapsed( container->collapsed() );
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorContainerType::Row:
|
|
{
|
|
QWidget *rowWidget = new QWidget();
|
|
widgetName = QStringLiteral( "Row" );
|
|
myContainer = rowWidget;
|
|
newWidgetInfo.widget = myContainer;
|
|
removeLayoutMargin = true;
|
|
columnCount = container->children().size();
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorContainerType::Tab:
|
|
{
|
|
myContainer = new QWidget();
|
|
|
|
QgsScrollArea *scrollArea = new QgsScrollArea( parent );
|
|
|
|
scrollArea->setWidget( myContainer );
|
|
scrollArea->setWidgetResizable( true );
|
|
scrollArea->setFrameShape( QFrame::NoFrame );
|
|
widgetName = QStringLiteral( "QScrollArea QWidget" );
|
|
|
|
newWidgetInfo.widget = scrollArea;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( container->backgroundColor().isValid() )
|
|
{
|
|
QString style {QStringLiteral( "background-color: %1;" ).arg( container->backgroundColor().name() )};
|
|
newWidgetInfo.widget->setStyleSheet( style );
|
|
}
|
|
|
|
QGridLayout *gbLayout = new QGridLayout();
|
|
if ( removeLayoutMargin )
|
|
gbLayout->setContentsMargins( 0, 0, 0, 0 );
|
|
myContainer->setLayout( gbLayout );
|
|
|
|
int row = 0;
|
|
int column = 0;
|
|
bool addSpacer = true;
|
|
|
|
const QList<QgsAttributeEditorElement *> children = container->children();
|
|
|
|
for ( QgsAttributeEditorElement *childDef : children )
|
|
{
|
|
WidgetInfo widgetInfo = createWidgetFromDef( childDef, myContainer, vl, context );
|
|
|
|
if ( childDef->type() == Qgis::AttributeEditorType::Container )
|
|
{
|
|
QgsAttributeEditorContainer *containerDef = static_cast<QgsAttributeEditorContainer *>( childDef );
|
|
if ( containerDef->visibilityExpression().enabled() || containerDef->collapsedExpression().enabled() )
|
|
{
|
|
registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().enabled() ? containerDef->visibilityExpression().data() : QgsExpression(), containerDef->collapsed(), containerDef->collapsedExpression().enabled() ? containerDef->collapsedExpression().data() : QgsExpression() ) );
|
|
}
|
|
}
|
|
|
|
// column containing the actual widget, not the label
|
|
int widgetColumn = column;
|
|
|
|
if ( widgetInfo.labelText.isNull() || ! widgetInfo.showLabel )
|
|
{
|
|
gbLayout->addWidget( widgetInfo.widget, row, column, 1, 2 );
|
|
widgetColumn = column + 1;
|
|
column += 2;
|
|
}
|
|
else
|
|
{
|
|
QLabel *mypLabel = new QLabel( widgetInfo.labelText );
|
|
|
|
if ( widgetInfo.labelStyle.overrideColor )
|
|
{
|
|
if ( widgetInfo.labelStyle.color.isValid() )
|
|
{
|
|
mypLabel->setStyleSheet( QStringLiteral( "QLabel { color: %1; }" ).arg( widgetInfo.labelStyle.color.name( QColor::HexArgb ) ) );
|
|
}
|
|
}
|
|
|
|
if ( widgetInfo.labelStyle.overrideFont )
|
|
{
|
|
mypLabel->setFont( widgetInfo.labelStyle.font );
|
|
}
|
|
|
|
// Alias DD overrides
|
|
if ( childDef->type() == Qgis::AttributeEditorType::Field )
|
|
{
|
|
const QgsAttributeEditorField *fieldDef { static_cast<QgsAttributeEditorField *>( childDef ) };
|
|
const QgsFields fields = vl->fields();
|
|
const int fldIdx = fieldDef->idx();
|
|
if ( fldIdx < fields.count() && fldIdx >= 0 )
|
|
{
|
|
const QString fieldName { fields.at( fldIdx ).name() };
|
|
if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Alias ) )
|
|
{
|
|
const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Alias ) };
|
|
if ( property.isActive() )
|
|
{
|
|
mLabelDataDefinedProperties[ mypLabel ] = property;
|
|
}
|
|
}
|
|
if ( mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).hasProperty( QgsEditFormConfig::DataDefinedProperty::Editable ) )
|
|
{
|
|
const QgsProperty property { mLayer->editFormConfig().dataDefinedFieldProperties( fieldName ).property( QgsEditFormConfig::DataDefinedProperty::Editable ) };
|
|
if ( property.isActive() )
|
|
{
|
|
mEditableDataDefinedProperties[ widgetInfo.widget ] = property;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mypLabel->setToolTip( widgetInfo.toolTip );
|
|
if ( columnCount > 1 && !widgetInfo.labelOnTop )
|
|
{
|
|
mypLabel->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
|
|
}
|
|
|
|
mypLabel->setBuddy( widgetInfo.widget );
|
|
|
|
if ( widgetInfo.labelOnTop )
|
|
{
|
|
widgetColumn = column + 1;
|
|
QVBoxLayout *c = new QVBoxLayout();
|
|
mypLabel->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
|
|
c->layout()->addWidget( mypLabel );
|
|
c->layout()->addWidget( widgetInfo.widget );
|
|
gbLayout->addLayout( c, row, column, 1, 2 );
|
|
column += 2;
|
|
}
|
|
else
|
|
{
|
|
widgetColumn = column + 1;
|
|
gbLayout->addWidget( mypLabel, row, column++ );
|
|
gbLayout->addWidget( widgetInfo.widget, row, column++ );
|
|
}
|
|
}
|
|
|
|
const int childHorizontalStretch = childDef->horizontalStretch();
|
|
const int existingColumnStretch = gbLayout->columnStretch( widgetColumn );
|
|
if ( childHorizontalStretch > 0 && childHorizontalStretch > existingColumnStretch )
|
|
{
|
|
gbLayout->setColumnStretch( widgetColumn, childHorizontalStretch );
|
|
}
|
|
|
|
if ( childDef->verticalStretch() > 0 && childDef->verticalStretch() > gbLayout->rowStretch( row ) )
|
|
{
|
|
gbLayout->setRowStretch( row, childDef->verticalStretch() );
|
|
}
|
|
|
|
if ( column >= columnCount * 2 )
|
|
{
|
|
column = 0;
|
|
row += 1;
|
|
}
|
|
|
|
if ( widgetInfo.widget
|
|
&& widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Fixed
|
|
&& widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Maximum
|
|
&& widgetInfo.widget->sizePolicy().verticalPolicy() != QSizePolicy::Preferred )
|
|
addSpacer = false;
|
|
|
|
// we consider all relation editors should be expanding
|
|
if ( qobject_cast<QgsAttributeFormRelationEditorWidget *>( widgetInfo.widget ) )
|
|
addSpacer = false;
|
|
}
|
|
|
|
if ( addSpacer )
|
|
{
|
|
QWidget *spacer = new QWidget();
|
|
spacer->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred );
|
|
gbLayout->addWidget( spacer, ++row, 0 );
|
|
gbLayout->setRowStretch( row, 1 );
|
|
}
|
|
|
|
newWidgetInfo.labelText = QString();
|
|
newWidgetInfo.labelOnTop = true;
|
|
newWidgetInfo.showLabel = widgetDef->showLabel();
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::QmlElement:
|
|
{
|
|
const QgsAttributeEditorQmlElement *elementDef = static_cast<const QgsAttributeEditorQmlElement *>( widgetDef );
|
|
|
|
QgsQmlWidgetWrapper *qmlWrapper = new QgsQmlWidgetWrapper( mLayer, nullptr, this );
|
|
qmlWrapper->setQmlCode( elementDef->qmlCode() );
|
|
context.setAttributeFormMode( mMode );
|
|
qmlWrapper->setContext( context );
|
|
|
|
mWidgets.append( qmlWrapper );
|
|
|
|
newWidgetInfo.widget = qmlWrapper->widget();
|
|
newWidgetInfo.labelText = elementDef->name();
|
|
newWidgetInfo.labelOnTop = true;
|
|
newWidgetInfo.showLabel = widgetDef->showLabel();
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::HtmlElement:
|
|
{
|
|
const QgsAttributeEditorHtmlElement *elementDef = static_cast<const QgsAttributeEditorHtmlElement *>( widgetDef );
|
|
|
|
QgsHtmlWidgetWrapper *htmlWrapper = new QgsHtmlWidgetWrapper( mLayer, nullptr, this );
|
|
context.setAttributeFormMode( mMode );
|
|
htmlWrapper->setHtmlCode( elementDef->htmlCode() );
|
|
htmlWrapper->reinitWidget();
|
|
mWidgets.append( htmlWrapper );
|
|
|
|
newWidgetInfo.widget = htmlWrapper->widget();
|
|
newWidgetInfo.labelText = elementDef->name();
|
|
newWidgetInfo.labelOnTop = true;
|
|
newWidgetInfo.showLabel = widgetDef->showLabel();
|
|
mNeedsGeometry |= htmlWrapper->needsGeometry();
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::TextElement:
|
|
{
|
|
const QgsAttributeEditorTextElement *elementDef = static_cast<const QgsAttributeEditorTextElement *>( widgetDef );
|
|
|
|
QgsTextWidgetWrapper *textWrapper = new QgsTextWidgetWrapper( mLayer, nullptr, this );
|
|
context.setAttributeFormMode( mMode );
|
|
textWrapper->setText( elementDef->text() );
|
|
textWrapper->reinitWidget();
|
|
mWidgets.append( textWrapper );
|
|
|
|
newWidgetInfo.widget = textWrapper->widget();
|
|
newWidgetInfo.labelText = elementDef->name();
|
|
newWidgetInfo.labelOnTop = false;
|
|
newWidgetInfo.showLabel = widgetDef->showLabel();
|
|
mNeedsGeometry |= textWrapper->needsGeometry();
|
|
break;
|
|
}
|
|
|
|
case Qgis::AttributeEditorType::SpacerElement:
|
|
{
|
|
const QgsAttributeEditorSpacerElement *elementDef = static_cast<const QgsAttributeEditorSpacerElement *>( widgetDef );
|
|
QgsSpacerWidgetWrapper *spacerWrapper = new QgsSpacerWidgetWrapper( mLayer, nullptr, this );
|
|
spacerWrapper->setDrawLine( elementDef->drawLine() );
|
|
context.setAttributeFormMode( mMode );
|
|
mWidgets.append( spacerWrapper );
|
|
|
|
newWidgetInfo.widget = spacerWrapper->widget();
|
|
newWidgetInfo.labelOnTop = false;
|
|
newWidgetInfo.showLabel = false;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
QgsDebugError( QStringLiteral( "Unknown attribute editor widget type encountered..." ) );
|
|
break;
|
|
}
|
|
|
|
return newWidgetInfo;
|
|
}
|
|
|
|
void QgsAttributeForm::createWrappers()
|
|
{
|
|
QList<QWidget *> myWidgets = findChildren<QWidget *>();
|
|
const QList<QgsField> fields = mLayer->fields().toList();
|
|
|
|
const auto constMyWidgets = myWidgets;
|
|
for ( QWidget *myWidget : constMyWidgets )
|
|
{
|
|
// Check the widget's properties for a relation definition
|
|
QVariant vRel = myWidget->property( "qgisRelation" );
|
|
if ( vRel.isValid() )
|
|
{
|
|
QgsRelationManager *relMgr = QgsProject::instance()->relationManager();
|
|
QgsRelation relation = relMgr->relation( vRel.toString() );
|
|
if ( relation.isValid() )
|
|
{
|
|
QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( mLayer, relation, myWidget, this );
|
|
rww->setConfig( mLayer->editFormConfig().widgetConfig( relation.id() ) );
|
|
rww->setContext( mContext );
|
|
rww->widget(); // Will initialize the widget
|
|
mWidgets.append( rww );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const auto constFields = fields;
|
|
for ( const QgsField &field : constFields )
|
|
{
|
|
if ( field.name() == myWidget->objectName() )
|
|
{
|
|
int idx = mLayer->fields().lookupField( field.name() );
|
|
|
|
QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( mLayer, idx, myWidget, this, mContext );
|
|
mWidgets.append( eww );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::afterWidgetInit()
|
|
{
|
|
bool isFirstEww = true;
|
|
|
|
const auto constMWidgets = mWidgets;
|
|
for ( QgsWidgetWrapper *ww : constMWidgets )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
|
|
if ( eww )
|
|
{
|
|
if ( isFirstEww )
|
|
{
|
|
setFocusProxy( eww->widget() );
|
|
isFirstEww = false;
|
|
}
|
|
|
|
connect( eww, &QgsEditorWidgetWrapper::valuesChanged, this, &QgsAttributeForm::onAttributeChanged, Qt::UniqueConnection );
|
|
connect( eww, &QgsEditorWidgetWrapper::constraintStatusChanged, this, &QgsAttributeForm::onConstraintStatusChanged, Qt::UniqueConnection );
|
|
}
|
|
else
|
|
{
|
|
QgsRelationWidgetWrapper *relationWidgetWrapper = qobject_cast<QgsRelationWidgetWrapper *>( ww );
|
|
if ( relationWidgetWrapper )
|
|
{
|
|
connect( relationWidgetWrapper, &QgsRelationWidgetWrapper::relatedFeaturesChanged, this, &QgsAttributeForm::onRelatedFeaturesChanged, static_cast<Qt::ConnectionType>( Qt::UniqueConnection | Qt::QueuedConnection ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool QgsAttributeForm::eventFilter( QObject *object, QEvent *e )
|
|
{
|
|
Q_UNUSED( object )
|
|
|
|
if ( e->type() == QEvent::KeyPress )
|
|
{
|
|
QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>( e );
|
|
if ( keyEvent && keyEvent->key() == Qt::Key_Escape )
|
|
{
|
|
// Re-emit to this form so it will be forwarded to parent
|
|
event( e );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void QgsAttributeForm::scanForEqualAttributes( QgsFeatureIterator &fit,
|
|
QSet< int > &mixedValueFields,
|
|
QHash< int, QVariant > &fieldSharedValues ) const
|
|
{
|
|
mixedValueFields.clear();
|
|
fieldSharedValues.clear();
|
|
|
|
QgsFeature f;
|
|
bool first = true;
|
|
while ( fit.nextFeature( f ) )
|
|
{
|
|
for ( int i = 0; i < mLayer->fields().count(); ++i )
|
|
{
|
|
if ( mixedValueFields.contains( i ) )
|
|
continue;
|
|
|
|
if ( first )
|
|
{
|
|
fieldSharedValues[i] = f.attribute( i );
|
|
}
|
|
else
|
|
{
|
|
if ( fieldSharedValues.value( i ) != f.attribute( i ) )
|
|
{
|
|
fieldSharedValues.remove( i );
|
|
mixedValueFields.insert( i );
|
|
}
|
|
}
|
|
}
|
|
first = false;
|
|
|
|
if ( mixedValueFields.count() == mLayer->fields().count() )
|
|
{
|
|
// all attributes are mixed, no need to keep scanning
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void QgsAttributeForm::layerSelectionChanged()
|
|
{
|
|
switch ( mMode )
|
|
{
|
|
case QgsAttributeEditorContext::SingleEditMode:
|
|
case QgsAttributeEditorContext::IdentifyMode:
|
|
case QgsAttributeEditorContext::AddFeatureMode:
|
|
case QgsAttributeEditorContext::FixAttributeMode:
|
|
case QgsAttributeEditorContext::SearchMode:
|
|
case QgsAttributeEditorContext::AggregateSearchMode:
|
|
break;
|
|
|
|
case QgsAttributeEditorContext::MultiEditMode:
|
|
resetMultiEdit( true );
|
|
break;
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::setMultiEditFeatureIds( const QgsFeatureIds &fids )
|
|
{
|
|
mIsSettingMultiEditFeatures = true;
|
|
mMultiEditFeatureIds = fids;
|
|
|
|
if ( fids.isEmpty() )
|
|
{
|
|
// no selected features
|
|
QMultiMap< int, QgsAttributeFormEditorWidget * >::const_iterator wIt = mFormEditorWidgets.constBegin();
|
|
for ( ; wIt != mFormEditorWidgets.constEnd(); ++ wIt )
|
|
{
|
|
wIt.value()->initialize( QVariant() );
|
|
}
|
|
mIsSettingMultiEditFeatures = false;
|
|
return;
|
|
}
|
|
|
|
QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFids( fids ) );
|
|
|
|
// Scan through all features to determine which attributes are initially the same
|
|
QSet< int > mixedValueFields;
|
|
QHash< int, QVariant > fieldSharedValues;
|
|
scanForEqualAttributes( fit, mixedValueFields, fieldSharedValues );
|
|
|
|
// also fetch just first feature
|
|
fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( *fids.constBegin() ) );
|
|
QgsFeature firstFeature;
|
|
fit.nextFeature( firstFeature );
|
|
|
|
// Make this feature the current form feature or the constraints will be evaluated
|
|
// on a possibly wrong previously selected/current feature
|
|
if ( mCurrentFormFeature.id() != firstFeature.id( ) )
|
|
{
|
|
setFeature( firstFeature );
|
|
}
|
|
|
|
const auto constMixedValueFields = mixedValueFields;
|
|
for ( int fieldIndex : std::as_const( mixedValueFields ) )
|
|
{
|
|
const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( fieldIndex );
|
|
if ( formEditorWidgets.isEmpty() )
|
|
continue;
|
|
|
|
const QStringList additionalFields = formEditorWidgets.first()->editorWidget()->additionalFields();
|
|
QVariantList additionalFieldValues;
|
|
for ( const QString &additionalField : additionalFields )
|
|
additionalFieldValues << firstFeature.attribute( additionalField );
|
|
|
|
for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
|
|
w->initialize( firstFeature.attribute( fieldIndex ), true, additionalFieldValues );
|
|
}
|
|
QHash< int, QVariant >::const_iterator sharedValueIt = fieldSharedValues.constBegin();
|
|
for ( ; sharedValueIt != fieldSharedValues.constEnd(); ++sharedValueIt )
|
|
{
|
|
const QList<QgsAttributeFormEditorWidget *> formEditorWidgets = mFormEditorWidgets.values( sharedValueIt.key() );
|
|
if ( formEditorWidgets.isEmpty() )
|
|
continue;
|
|
|
|
bool mixed = false;
|
|
const QStringList additionalFields = formEditorWidgets.first()->editorWidget()->additionalFields();
|
|
for ( const QString &additionalField : additionalFields )
|
|
{
|
|
int index = mLayer->fields().indexFromName( additionalField );
|
|
if ( constMixedValueFields.contains( index ) )
|
|
{
|
|
// if additional field are mixed, it is considered as mixed
|
|
mixed = true;
|
|
break;
|
|
}
|
|
}
|
|
QVariantList additionalFieldValues;
|
|
if ( mixed )
|
|
{
|
|
for ( const QString &additionalField : additionalFields )
|
|
additionalFieldValues << firstFeature.attribute( additionalField );
|
|
for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
|
|
w->initialize( firstFeature.attribute( sharedValueIt.key() ), true, additionalFieldValues );
|
|
}
|
|
else
|
|
{
|
|
for ( const QString &additionalField : additionalFields )
|
|
{
|
|
int index = mLayer->fields().indexFromName( additionalField );
|
|
Q_ASSERT( fieldSharedValues.contains( index ) );
|
|
additionalFieldValues << fieldSharedValues.value( index );
|
|
}
|
|
for ( QgsAttributeFormEditorWidget *w : formEditorWidgets )
|
|
w->initialize( sharedValueIt.value(), false, additionalFieldValues );
|
|
}
|
|
}
|
|
|
|
setMultiEditFeatureIdsRelations( fids );
|
|
|
|
mIsSettingMultiEditFeatures = false;
|
|
}
|
|
|
|
void QgsAttributeForm::setMessageBar( QgsMessageBar *messageBar )
|
|
{
|
|
if ( mOwnsMessageBar )
|
|
delete mMessageBar;
|
|
mOwnsMessageBar = false;
|
|
mMessageBar = messageBar;
|
|
}
|
|
|
|
QString QgsAttributeForm::aggregateFilter() const
|
|
{
|
|
if ( mMode != QgsAttributeEditorContext::AggregateSearchMode )
|
|
{
|
|
Q_ASSERT( false );
|
|
}
|
|
|
|
QStringList filters;
|
|
for ( QgsAttributeFormWidget *widget : mFormWidgets )
|
|
{
|
|
QString filter = widget->currentFilterExpression();
|
|
if ( !filter.isNull() )
|
|
filters << '(' + filter + ')';
|
|
}
|
|
|
|
return filters.join( QLatin1String( " AND " ) );
|
|
}
|
|
|
|
void QgsAttributeForm::setExtraContextScope( QgsExpressionContextScope *extraScope )
|
|
{
|
|
mExtraContextScope.reset( extraScope );
|
|
}
|
|
|
|
void QgsAttributeForm::ContainerInformation::apply( QgsExpressionContext *expressionContext )
|
|
{
|
|
|
|
const bool newVisibility = expression.evaluate( expressionContext ).toBool();
|
|
|
|
if ( expression.isValid() && ! expression.hasEvalError() && newVisibility != isVisible )
|
|
{
|
|
if ( tabWidget )
|
|
{
|
|
tabWidget->setTabVisible( widget, newVisibility );
|
|
}
|
|
else
|
|
{
|
|
widget->setVisible( newVisibility );
|
|
}
|
|
|
|
isVisible = newVisibility;
|
|
}
|
|
|
|
const bool newCollapsedState = collapsedExpression.evaluate( expressionContext ).toBool();
|
|
|
|
if ( collapsedExpression.isValid() && ! collapsedExpression.hasEvalError() && newCollapsedState != isCollapsed )
|
|
{
|
|
|
|
if ( QgsCollapsibleGroupBoxBasic * collapsibleGroupBox { qobject_cast<QgsCollapsibleGroupBoxBasic *>( widget ) } )
|
|
{
|
|
collapsibleGroupBox->setCollapsed( newCollapsedState );
|
|
isCollapsed = newCollapsedState;
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateJoinedFields( const QgsEditorWidgetWrapper &eww )
|
|
{
|
|
if ( !eww.layer()->fields().exists( eww.fieldIdx() ) )
|
|
return;
|
|
|
|
QgsFeature formFeature;
|
|
QgsField field = eww.layer()->fields().field( eww.fieldIdx() );
|
|
QList<const QgsVectorLayerJoinInfo *> infos = eww.layer()->joinBuffer()->joinsWhereFieldIsId( field );
|
|
|
|
if ( infos.count() == 0 || !currentFormValuesFeature( formFeature ) )
|
|
return;
|
|
|
|
const QString hint = tr( "No feature joined" );
|
|
const auto constInfos = infos;
|
|
for ( const QgsVectorLayerJoinInfo *info : constInfos )
|
|
{
|
|
if ( !info->isDynamicFormEnabled() )
|
|
continue;
|
|
|
|
QgsFeature joinFeature = mLayer->joinBuffer()->joinedFeatureOf( info, formFeature );
|
|
|
|
mJoinedFeatures[info] = joinFeature;
|
|
|
|
if ( info->hasSubset() )
|
|
{
|
|
const QStringList subsetNames = QgsVectorLayerJoinInfo::joinFieldNamesSubset( *info );
|
|
|
|
const auto constSubsetNames = subsetNames;
|
|
for ( const QString &field : constSubsetNames )
|
|
{
|
|
QString prefixedName = info->prefixedFieldName( field );
|
|
QVariant val;
|
|
QString hintText = hint;
|
|
|
|
if ( joinFeature.isValid() )
|
|
{
|
|
val = joinFeature.attribute( field );
|
|
hintText.clear();
|
|
}
|
|
|
|
changeAttribute( prefixedName, val, hintText );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const QgsFields joinFields = joinFeature.fields();
|
|
for ( const QgsField &field : joinFields )
|
|
{
|
|
QString prefixedName = info->prefixedFieldName( field );
|
|
QVariant val;
|
|
QString hintText = hint;
|
|
|
|
if ( joinFeature.isValid() )
|
|
{
|
|
val = joinFeature.attribute( field.name() );
|
|
hintText.clear();
|
|
}
|
|
|
|
changeAttribute( prefixedName, val, hintText );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool QgsAttributeForm::fieldIsEditable( int fieldIndex ) const
|
|
{
|
|
return QgsVectorLayerUtils::fieldIsEditable( mLayer, fieldIndex, mFeature );
|
|
}
|
|
|
|
void QgsAttributeForm::updateFieldDependencies()
|
|
{
|
|
mDefaultValueDependencies.clear();
|
|
mVirtualFieldsDependencies.clear();
|
|
mRelatedLayerFieldsDependencies.clear();
|
|
|
|
//create defaultValueDependencies
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( ! eww )
|
|
continue;
|
|
|
|
updateFieldDependenciesDefaultValue( eww );
|
|
updateFieldDependenciesVirtualFields( eww );
|
|
updateRelatedLayerFieldsDependencies( eww );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateFieldDependenciesDefaultValue( QgsEditorWidgetWrapper *eww )
|
|
{
|
|
QgsExpression exp( eww->field().defaultValueDefinition().expression() );
|
|
|
|
if ( exp.needsGeometry() )
|
|
mNeedsGeometry = true;
|
|
|
|
//if a function requires all attributes, it should have the dependency of every field change
|
|
if ( exp.referencedColumns().contains( QgsFeatureRequest::ALL_ATTRIBUTES ) )
|
|
{
|
|
const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );
|
|
|
|
for ( const int id : allAttributeIds )
|
|
{
|
|
mDefaultValueDependencies.insertMulti( id, eww );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//otherwise just enter for the field depending on
|
|
const QSet<QString> referencedColumns = exp.referencedColumns();
|
|
for ( const QString &referencedColumn : referencedColumns )
|
|
{
|
|
mDefaultValueDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateFieldDependenciesVirtualFields( QgsEditorWidgetWrapper *eww )
|
|
{
|
|
QString expressionField = eww->layer()->expressionField( eww->fieldIdx() );
|
|
if ( expressionField.isEmpty() )
|
|
return;
|
|
|
|
QgsExpression exp( expressionField );
|
|
|
|
if ( exp.needsGeometry() )
|
|
mNeedsGeometry = true;
|
|
|
|
//if a function requires all attributes, it should have the dependency of every field change
|
|
if ( exp.referencedColumns().contains( QgsFeatureRequest::ALL_ATTRIBUTES ) )
|
|
{
|
|
const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );
|
|
|
|
for ( const int id : allAttributeIds )
|
|
{
|
|
mVirtualFieldsDependencies.insertMulti( id, eww );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//otherwise just enter for the field depending on
|
|
const QSet<QString> referencedColumns = exp.referencedColumns();
|
|
for ( const QString &referencedColumn : referencedColumns )
|
|
{
|
|
mVirtualFieldsDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateRelatedLayerFieldsDependencies( QgsEditorWidgetWrapper *eww )
|
|
{
|
|
if ( eww )
|
|
{
|
|
QString expressionField = eww->layer()->expressionField( eww->fieldIdx() );
|
|
if ( expressionField.contains( QStringLiteral( "relation_aggregate" ) )
|
|
|| expressionField.contains( QStringLiteral( "get_features" ) ) )
|
|
mRelatedLayerFieldsDependencies.insert( eww );
|
|
}
|
|
else
|
|
{
|
|
mRelatedLayerFieldsDependencies.clear();
|
|
//create defaultValueDependencies
|
|
for ( QgsWidgetWrapper *ww : std::as_const( mWidgets ) )
|
|
{
|
|
QgsEditorWidgetWrapper *editorWidgetWrapper = qobject_cast<QgsEditorWidgetWrapper *>( ww );
|
|
if ( ! editorWidgetWrapper )
|
|
continue;
|
|
|
|
updateRelatedLayerFieldsDependencies( editorWidgetWrapper );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::setMultiEditFeatureIdsRelations( const QgsFeatureIds &fids )
|
|
{
|
|
for ( QgsAttributeFormWidget *formWidget : mFormWidgets )
|
|
{
|
|
QgsAttributeFormRelationEditorWidget *relationEditorWidget = dynamic_cast<QgsAttributeFormRelationEditorWidget *>( formWidget );
|
|
if ( !relationEditorWidget )
|
|
continue;
|
|
|
|
relationEditorWidget->setMultiEditFeatureIds( fids );
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::updateIcon( QgsEditorWidgetWrapper *eww )
|
|
{
|
|
if ( !eww->widget() || !mIconMap[eww->widget()] )
|
|
return;
|
|
|
|
// no icon by default
|
|
mIconMap[eww->widget()]->hide();
|
|
|
|
if ( !eww->widget()->isEnabled() && mLayer->isEditable() )
|
|
{
|
|
if ( mLayer->fields().fieldOrigin( eww->fieldIdx() ) == Qgis::FieldOrigin::Join )
|
|
{
|
|
int srcFieldIndex;
|
|
const QgsVectorLayerJoinInfo *info = mLayer->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), mLayer->fields(), srcFieldIndex );
|
|
|
|
if ( !info )
|
|
return;
|
|
|
|
if ( !info->isEditable() )
|
|
{
|
|
const QString file = QStringLiteral( "/mIconJoinNotEditable.svg" );
|
|
const QString tooltip = tr( "Join settings do not allow editing" );
|
|
reloadIcon( file, tooltip, mIconMap[eww->widget()] );
|
|
}
|
|
else if ( mMode == QgsAttributeEditorContext::AddFeatureMode && !info->hasUpsertOnEdit() )
|
|
{
|
|
const QString file = QStringLiteral( "mIconJoinHasNotUpsertOnEdit.svg" );
|
|
const QString tooltip = tr( "Join settings do not allow upsert on edit" );
|
|
reloadIcon( file, tooltip, mIconMap[eww->widget()] );
|
|
}
|
|
else if ( !info->joinLayer()->isEditable() )
|
|
{
|
|
const QString file = QStringLiteral( "/mIconJoinedLayerNotEditable.svg" );
|
|
const QString tooltip = tr( "Joined layer is not toggled editable" );
|
|
reloadIcon( file, tooltip, mIconMap[eww->widget()] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsAttributeForm::reloadIcon( const QString &file, const QString &tooltip, QSvgWidget *sw )
|
|
{
|
|
sw->load( QgsApplication::iconPath( file ) );
|
|
sw->setToolTip( tooltip );
|
|
sw->show();
|
|
}
|