QGIS/src/gui/qgsattributeform.cpp
2018-03-22 16:23:51 +10:00

2143 lines
65 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 "qgsattributeforminterface.h"
#include "qgsattributeformlegacyinterface.h"
#include "qgsattributeformrelationeditorwidget.h"
#include "qgseditorwidgetregistry.h"
#include "qgsfeatureiterator.h"
#include "qgsproject.h"
#include "qgspythonrunner.h"
#include "qgsrelationwidgetwrapper.h"
#include "qgsvectordataprovider.h"
#include "qgsattributeformeditorwidget.h"
#include "qgsmessagebar.h"
#include "qgsmessagebaritem.h"
#include "qgseditorwidgetwrapper.h"
#include "qgsrelationmanager.h"
#include "qgslogger.h"
#include "qgstabwidget.h"
#include "qgssettings.h"
#include "qgsscrollarea.h"
#include "qgsgui.h"
#include "qgsvectorlayerjoinbuffer.h"
#include "qgsvectorlayerutils.h"
#include <QDir>
#include <QTextStream>
#include <QFileInfo>
#include <QFile>
#include <QFormLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QKeyEvent>
#include <QLabel>
#include <QPushButton>
#include <QUiLoader>
#include <QMessageBox>
#include <QToolButton>
#include <QMenu>
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( 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 );
// constraints management
updateAllConstraints();
}
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 == 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( QgsAttributeForm::Mode mode )
{
if ( mode == mMode )
return;
if ( mMode == 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 == 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 : qgis::as_const( mFormWidgets ) )
{
switch ( mode )
{
case QgsAttributeForm::SingleEditMode:
w->setMode( QgsAttributeFormWidget::DefaultMode );
break;
case QgsAttributeForm::AddFeatureMode:
w->setMode( QgsAttributeFormWidget::DefaultMode );
break;
case QgsAttributeForm::MultiEditMode:
w->setMode( QgsAttributeFormWidget::MultiEditMode );
break;
case QgsAttributeForm::SearchMode:
w->setMode( QgsAttributeFormWidget::SearchMode );
break;
case QgsAttributeForm::AggregateSearchMode:
w->setMode( QgsAttributeFormWidget::AggregateSearchMode );
break;
case QgsAttributeForm::IdentifyMode:
w->setMode( QgsAttributeFormWidget::DefaultMode );
break;
}
}
bool relationWidgetsVisible = ( mMode != QgsAttributeForm::MultiEditMode && mMode != QgsAttributeForm::AggregateSearchMode );
for ( QgsAttributeFormRelationEditorWidget *w : findChildren< QgsAttributeFormRelationEditorWidget * >() )
{
w->setVisible( relationWidgetsVisible );
}
switch ( mode )
{
case QgsAttributeForm::SingleEditMode:
setFeature( mFeature );
mSearchButtonBox->setVisible( false );
break;
case QgsAttributeForm::AddFeatureMode:
synchronizeEnabledState();
mSearchButtonBox->setVisible( false );
break;
case QgsAttributeForm::MultiEditMode:
resetMultiEdit( false );
synchronizeEnabledState();
mSearchButtonBox->setVisible( false );
break;
case QgsAttributeForm::SearchMode:
mSearchButtonBox->setVisible( true );
hideButtonBox();
break;
case QgsAttributeForm::AggregateSearchMode:
mSearchButtonBox->setVisible( false );
hideButtonBox();
break;
case QgsAttributeForm::IdentifyMode:
setFeature( mFeature );
mSearchButtonBox->setVisible( false );
break;
}
emit modeChanged( mMode );
}
void QgsAttributeForm::changeAttribute( const QString &field, const QVariant &value, const QString &hintText )
{
Q_FOREACH ( QgsWidgetWrapper *ww, mWidgets )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww && eww->field().name() == field )
{
eww->setValue( value );
eww->setHint( hintText );
}
}
}
void QgsAttributeForm::setFeature( const QgsFeature &feature )
{
mIsSettingFeature = true;
mFeature = feature;
switch ( mMode )
{
case SingleEditMode:
case IdentifyMode:
case AddFeatureMode:
{
resetValues();
synchronizeEnabledState();
Q_FOREACH ( QgsAttributeFormInterface *iface, mInterfaces )
{
iface->featureChanged();
}
break;
}
case MultiEditMode:
case SearchMode:
case AggregateSearchMode:
{
//ignore setFeature
break;
}
}
mIsSettingFeature = false;
}
bool QgsAttributeForm::saveEdits()
{
bool success = true;
bool changedLayer = false;
QgsFeature updatedFeature = QgsFeature( mFeature );
if ( mFeature.isValid() || mMode == AddFeatureMode )
{
bool doUpdate = false;
// An add dialog should perform an action by default
// and not only if attributes have "changed"
if ( mMode == AddFeatureMode )
doUpdate = true;
QgsAttributes src = mFeature.attributes();
QgsAttributes dst = mFeature.attributes();
Q_FOREACH ( QgsWidgetWrapper *ww, mWidgets )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww )
{
QVariant dstVar = dst.at( eww->fieldIdx() );
QVariant srcVar = eww->value();
// need to check dstVar.isNull() != srcVar.isNull()
// otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
// be careful- sometimes two null qvariants will be reported as not equal!! (e.g., different types)
bool changed = ( dstVar != srcVar && !dstVar.isNull() && !srcVar.isNull() )
|| ( dstVar.isNull() != srcVar.isNull() );
if ( changed && srcVar.isValid() && fieldIsEditable( eww->fieldIdx() ) )
{
dst[eww->fieldIdx()] = srcVar;
doUpdate = true;
}
}
}
updatedFeature.setAttributes( dst );
Q_FOREACH ( QgsAttributeFormInterface *iface, mInterfaces )
{
if ( !iface->acceptChanges( updatedFeature ) )
{
doUpdate = false;
}
}
if ( doUpdate )
{
if ( mMode == AddFeatureMode )
{
mFeature.setValid( true );
mLayer->beginEditCommand( mEditCommandMessage );
bool res = mLayer->addFeature( updatedFeature );
if ( res )
{
mFeature.setAttributes( updatedFeature.attributes() );
mLayer->endEditCommand();
setMode( 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 ( ( dst.at( i ) == src.at( i ) && dst.at( i ).isNull() == src.at( i ).isNull() ) // 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;
}
QgsDebugMsg( QString( "Updating field %1" ).arg( i ) );
QgsDebugMsg( QString( "dst:'%1' (type:%2, isNull:%3, isValid:%4)" )
.arg( dst.at( i ).toString(), dst.at( i ).typeName() ).arg( dst.at( i ).isNull() ).arg( dst.at( i ).isValid() ) );
QgsDebugMsg( QString( "src:'%1' (type:%2, isNull:%3, isValid:%4)" )
.arg( src.at( i ).toString(), src.at( i ).typeName() ).arg( src.at( i ).isNull() ).arg( src.at( i ).isValid() ) );
newValues[i] = dst.at( i );
oldValues[i] = src.at( i );
n++;
}
success = mLayer->changeAttributeValues( mFeature.id(), newValues, oldValues );
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;
}
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( 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( SingleEditMode );
emit filterExpressionSet( filter, FilterAnd );
}
void QgsAttributeForm::filterOrTriggered()
{
QString filter = createFilterExpression();
if ( filter.isEmpty() )
return;
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
setMode( SingleEditMode );
emit filterExpressionSet( filter, FilterOr );
}
void QgsAttributeForm::pushSelectedFeaturesMessage()
{
int count = mLayer->selectedFeatureCount();
if ( count > 0 )
{
mMessageBar->pushMessage( QString(),
tr( "%1 matching %2 selected" ).arg( count )
.arg( count == 1 ? tr( "feature" ) : tr( "features" ) ),
Qgis::Info,
messageTimeout() );
}
else
{
mMessageBar->pushMessage( QString(),
tr( "No matching features found" ),
Qgis::Warning,
messageTimeout() );
}
}
void QgsAttributeForm::runSearchSelect( QgsVectorLayer::SelectBehavior behavior )
{
QString filter = createFilterExpression();
if ( filter.isEmpty() )
return;
mLayer->selectByExpression( filter, behavior );
pushSelectedFeaturesMessage();
if ( mContext.formMode() == QgsAttributeEditorContext::Embed )
setMode( SingleEditMode );
}
void QgsAttributeForm::searchSetSelection()
{
runSearchSelect( QgsVectorLayer::SetSelection );
}
void QgsAttributeForm::searchAddToSelection()
{
runSearchSelect( QgsVectorLayer::AddToSelection );
}
void QgsAttributeForm::searchRemoveFromSelection()
{
runSearchSelect( QgsVectorLayer::RemoveFromSelection );
}
void QgsAttributeForm::searchIntersectSelection()
{
runSearchSelect( QgsVectorLayer::IntersectSelection );
}
bool QgsAttributeForm::saveMultiEdits()
{
//find changed attributes
QgsAttributeMap newAttributeValues;
QMap< int, QgsAttributeFormEditorWidget * >::const_iterator wIt = mFormEditorWidgets.constBegin();
for ( ; wIt != mFormEditorWidgets.constEnd(); ++ wIt )
{
QgsAttributeFormEditorWidget *w = wIt.value();
if ( !w->hasChanged() )
continue;
if ( !w->currentValue().isValid() // if the widget returns invalid (== do not change)
|| mLayer->editFormConfig().readOnly( wIt.key() ) ) // or the field cannot be edited ...
{
continue;
}
// let editor know we've accepted the changes
w->changesCommitted();
newAttributeValues.insert( wIt.key(), w->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;
Q_FOREACH ( QgsFeatureId fid, mMultiEditFeatureIds )
{
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::Success, messageTimeout() );
}
else
{
mLayer->destroyEditCommand();
mMultiEditMessageBarItem = new QgsMessageBarItem( tr( "Changes could not be applied." ), Qgis::Warning, messageTimeout() );
}
if ( !mButtonBox->isVisible() )
mMessageBar->pushItem( mMultiEditMessageBarItem );
return success;
}
bool QgsAttributeForm::save()
{
if ( mIsSaving )
return true;
// 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 SingleEditMode:
case IdentifyMode:
case MultiEditMode:
if ( !mDirty )
return true;
break;
case AddFeatureMode:
case SearchMode:
case 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 SingleEditMode:
case IdentifyMode:
case AddFeatureMode:
case SearchMode:
case AggregateSearchMode:
success = saveEdits();
break;
case MultiEditMode:
success = saveMultiEdits();
break;
}
mIsSaving = false;
mUnsavedMultiEditChanges = false;
mDirty = false;
return success;
}
void QgsAttributeForm::resetValues()
{
mValuesInitialized = false;
Q_FOREACH ( QgsWidgetWrapper *ww, mWidgets )
{
ww->setFeature( mFeature );
}
mValuesInitialized = true;
mDirty = false;
}
void QgsAttributeForm::resetSearch()
{
Q_FOREACH ( QgsAttributeFormEditorWidget *w, findChildren< QgsAttributeFormEditorWidget * >() )
{
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 : qgis::as_const( mFormWidgets ) )
{
QString filter = w->currentFilterExpression();
if ( !filter.isEmpty() )
filters << filter;
}
if ( filters.isEmpty() )
return QString();
QString filter = filters.join( QStringLiteral( ") AND (" ) ).prepend( '(' ).append( ')' );
return filter;
}
void QgsAttributeForm::onAttributeChanged( const QVariant &value )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( sender() );
Q_ASSERT( eww );
bool signalEmitted = false;
if ( mValuesInitialized )
mDirty = true;
switch ( mMode )
{
case SingleEditMode:
case IdentifyMode:
case AddFeatureMode:
{
Q_NOWARN_DEPRECATED_PUSH
emit attributeChanged( eww->field().name(), value );
Q_NOWARN_DEPRECATED_PUSH
emit widgetValueChanged( eww->field().name(), value, !mIsSettingFeature );
signalEmitted = true;
updateJoinedFields( *eww );
break;
}
case 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::Warning );
if ( !mButtonBox->isVisible() )
mMessageBar->pushItem( mMultiEditUnsavedMessageBarItem );
}
break;
}
case SearchMode:
case AggregateSearchMode:
//nothing to do
break;
}
updateConstraints( eww );
if ( !signalEmitted )
{
Q_NOWARN_DEPRECATED_PUSH
emit attributeChanged( eww->field().name(), value );
Q_NOWARN_DEPRECATED_PUSH
emit widgetValueChanged( eww->field().name(), value, !mIsSettingFeature );
}
}
void QgsAttributeForm::updateAllConstraints()
{
Q_FOREACH ( QgsWidgetWrapper *ww, mWidgets )
{
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 ( currentFormFeature( 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
synchronizeEnabledState();
mExpressionContext.setFeature( ft );
// Recheck visibility for all containers which are controlled by this value
const QVector<ContainerInformation *> infos = mContainerInformationDependency.value( eww->field().name() );
for ( ContainerInformation *info : infos )
{
info->apply( &mExpressionContext );
}
}
}
void QgsAttributeForm::updateConstraint( const QgsFeature &ft, QgsEditorWidgetWrapper *eww )
{
QgsFieldConstraints::ConstraintOrigin constraintOrigin = mLayer->isEditable() ? QgsFieldConstraints::ConstraintOriginNotSet : QgsFieldConstraints::ConstraintOriginLayer;
if ( eww->layer()->fields().fieldOrigin( eww->fieldIdx() ) == QgsFields::OriginJoin )
{
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 );
}
bool QgsAttributeForm::currentFormFeature( QgsFeature &feature )
{
bool rc = true;
feature = QgsFeature( mFeature );
QgsAttributes dst = feature.attributes();
for ( QgsWidgetWrapper *ww : qgis::as_const( mWidgets ) )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( !eww )
continue;
if ( dst.count() > eww->fieldIdx() )
{
QVariant dstVar = dst.at( eww->fieldIdx() );
QVariant srcVar = eww->value();
// need to check dstVar.isNull() != srcVar.isNull()
// otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
if ( ( dstVar != srcVar || dstVar.isNull() != srcVar.isNull() ) && srcVar.isValid() )
dst[eww->fieldIdx()] = srcVar;
}
else
{
rc = false;
break;
}
}
feature.setAttributes( dst );
return rc;
}
void QgsAttributeForm::registerContainerInformation( QgsAttributeForm::ContainerInformation *info )
{
mContainerVisibilityInformation.append( info );
const QSet<QString> referencedColumns = info->expression.referencedColumns();
for ( const QString &col : referencedColumns )
{
mContainerInformationDependency[ col ].append( info );
}
}
bool QgsAttributeForm::currentFormValidConstraints( QStringList &invalidFields, QStringList &descriptions )
{
bool valid( true );
for ( QgsWidgetWrapper *ww : qgis::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;
}
void QgsAttributeForm::onAttributeAdded( int idx )
{
mPreventFeatureRefresh = false;
if ( mFeature.isValid() )
{
QgsAttributes attrs = mFeature.attributes();
attrs.insert( idx, QVariant( 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::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 ).type() != layer()->fields().at( i ).type() )
{
attrs[i].convert( layer()->fields().at( i ).type() );
}
}
else
{
attrs[i] = QVariant( 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 );
QgsAttributeFormEditorWidget *formEditorWidget = mFormEditorWidgets.value( eww->fieldIdx() );
if ( formEditorWidget )
formEditorWidget->setConstraintStatus( constraint, description, err, result );
}
QList<QgsEditorWidgetWrapper *> QgsAttributeForm::constraintDependencies( QgsEditorWidgetWrapper *w )
{
QList<QgsEditorWidgetWrapper *> wDeps;
QString name = w->field().name();
// for each widget in the current form
for ( QgsWidgetWrapper *ww : qgis::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;
}
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::synchronizeEnabledState()
{
bool isEditable = ( mFeature.isValid()
|| mMode == AddFeatureMode
|| mMode == MultiEditMode ) && mLayer->isEditable();
for ( QgsWidgetWrapper *ww : qgis::as_const( mWidgets ) )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww )
{
QgsAttributeFormEditorWidget *formWidget = mFormEditorWidgets.value( eww->fieldIdx() );
if ( formWidget )
formWidget->setConstraintResultVisible( isEditable );
eww->setConstraintResultVisible( isEditable );
bool enabled = isEditable && fieldIsEditable( eww->fieldIdx() );
ww->setEnabled( enabled );
updateIcon( eww );
}
}
if ( mMode != SearchMode )
{
QStringList invalidFields, descriptions;
bool validConstraint = currentFormValidConstraints( invalidFields, descriptions );
isEditable = isEditable & validConstraint;
}
// 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;
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->setMargin( 0 );
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() == QgsEditFormConfig::UiFileLayout &&
!mLayer->editFormConfig().uiForm().isEmpty() )
{
QFile file( mLayer->editFormConfig().uiForm() );
if ( file.open( QFile::ReadOnly ) )
{
QUiLoader loader;
QFileInfo fi( mLayer->editFormConfig().uiForm() );
loader.setWorkingDirectory( fi.dir() );
formWidget = loader.load( &file, this );
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() == QgsEditFormConfig::TabLayout )
{
int row = 0;
int column = 0;
int columnCount = 1;
const QList<QgsAttributeEditorElement *> tabs = mLayer->editFormConfig().tabs();
for ( QgsAttributeEditorElement *widgDef : tabs )
{
if ( widgDef->type() == QgsAttributeEditorElement::AeTypeContainer )
{
QgsAttributeEditorContainer *containerDef = dynamic_cast<QgsAttributeEditorContainer *>( widgDef );
if ( !containerDef )
continue;
if ( containerDef->isGroupBox() )
{
tabWidget = nullptr;
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, formWidget, mLayer, mContext );
layout->addWidget( widgetInfo.widget, row, column, 1, 2 );
registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().data() ) );
column += 2;
}
else
{
if ( !tabWidget )
{
tabWidget = new QgsTabWidget();
layout->addWidget( tabWidget, row, column, 1, 2 );
column += 2;
}
QWidget *tabPage = new QWidget( tabWidget );
tabWidget->addTab( tabPage, widgDef->name() );
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 );
}
}
else
{
tabWidget = nullptr;
WidgetInfo widgetInfo = createWidgetFromDef( widgDef, container, mLayer, mContext );
QLabel *label = new QLabel( widgetInfo.labelText );
label->setToolTip( QStringLiteral( "<b>%1</b><p>%2</p>" ).arg( widgetInfo.labelText, widgetInfo.hint ) );
if ( columnCount > 1 && !widgetInfo.labelOnTop )
{
label->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
}
label->setBuddy( widgetInfo.widget );
if ( !widgetInfo.showLabel )
{
QVBoxLayout *c = new QVBoxLayout();
label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Fixed );
c->addWidget( widgetInfo.widget );
layout->addLayout( c, row, column, 1, 2 );
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 );
column += 2;
}
else
{
layout->addWidget( label, row, column++ );
layout->addWidget( widgetInfo.widget, row, column++ );
}
}
if ( column >= columnCount * 2 )
{
column = 0;
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 );
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 *l = new QLabel( fieldName );
l->setToolTip( QStringLiteral( "<b>%1</b><p>%2</p>" ).arg( fieldName, field.comment() ) );
QSvgWidget *i = new QSvgWidget();
i->setFixedSize( 18, 18 );
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 );
l->setBuddy( eww->widget() );
}
else
{
w = new QLabel( QStringLiteral( "<p style=\"color: red; font-style: italic;\">%1</p>" ).arg( tr( "Failed to create widget with type '%1'" ), widgetSetup.type() ) );
}
if ( w )
w->setObjectName( field.name() );
if ( eww )
{
addWidgetWrapper( eww );
mIconMap[eww->widget()] = i;
}
if ( labelOnTop )
{
gridLayout->addWidget( l, row++, 0, 1, 2 );
gridLayout->addWidget( w, row++, 0, 1, 2 );
gridLayout->addWidget( i, row++, 0, 1, 2 );
}
else
{
gridLayout->addWidget( l, row, 0 );
gridLayout->addWidget( w, row, 1 );
gridLayout->addWidget( i, row++, 2 );
}
}
Q_FOREACH ( const QgsRelation &rel, QgsProject::instance()->relationManager()->referencedRelations( mLayer ) )
{
QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( mLayer, rel, nullptr, this );
const QgsEditorWidgetSetup setup = QgsGui::editorWidgetRegistry()->findBest( mLayer, rel.id() );
rww->setConfig( setup.config() );
rww->setContext( mContext );
gridLayout->addWidget( rww->widget(), row++, 0, 1, 2 );
QgsAttributeFormRelationEditorWidget *formWidget = new QgsAttributeFormRelationEditorWidget( rww, this );
formWidget->createSearchWidgetWrappers( mContext );
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++;
}
}
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->setMargin( 0 );
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 *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 );
}
mSearchButtonBox->setVisible( mMode == SearchMode );
afterWidgetInit();
connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsAttributeForm::save );
connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsAttributeForm::resetValues );
connect( mLayer, &QgsVectorLayer::editingStarted, this, &QgsAttributeForm::synchronizeEnabledState );
connect( mLayer, &QgsVectorLayer::editingStopped, this, &QgsAttributeForm::synchronizeEnabledState );
Q_FOREACH ( QgsAttributeFormInterface *iface, mInterfaces )
{
iface->initForm();
}
if ( mContext.formMode() == QgsAttributeEditorContext::Embed || mMode == 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() != QgsEditFormConfig::CodeSourceNone )
{
QString initFunction = mLayer->editFormConfig().initFunction();
QString initFilePath = mLayer->editFormConfig().initFilePath();
QString initCode;
switch ( mLayer->editFormConfig().initCodeSource() )
{
case QgsEditFormConfig::CodeSourceFile:
if ( ! initFilePath.isEmpty() )
{
QFile inputFile( initFilePath );
if ( inputFile.open( QFile::ReadOnly ) )
{
// Read it into a string
QTextStream inf( &inputFile );
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 QgsEditFormConfig::CodeSourceDialog:
initCode = mLayer->editFormConfig().initCode();
if ( initCode.isEmpty() )
{
QgsLogger::warning( QStringLiteral( "The python code provided in the dialog is empty!" ) );
}
break;
case QgsEditFormConfig::CodeSourceEnvironment:
case QgsEditFormConfig::CodeSourceNone:
default:
// Nothing to do: the function code should be already in the environment
break;
}
// If we have a function code, run it
if ( ! initCode.isEmpty() )
{
QgsPythonRunner::run( initCode );
}
QgsPythonRunner::run( QStringLiteral( "import inspect" ) );
QString numArgs;
// Check for eval result
if ( QgsPythonRunner::eval( QStringLiteral( "len(inspect.getargspec(%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 );
QgsDebugMsg( QString( "running featureForm init: %1" ).arg( mPyFormVarName ) );
// 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;
switch ( widgetDef->type() )
{
case QgsAttributeEditorElement::AeTypeField:
{
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;
addWidgetWrapper( eww );
newWidgetInfo.widget->setObjectName( fields.at( fldIdx ).name() );
newWidgetInfo.hint = fields.at( fieldDef->idx() ).comment();
}
newWidgetInfo.labelOnTop = mLayer->editFormConfig().labelOnTop( fieldDef->idx() );
newWidgetInfo.labelText = mLayer->attributeDisplayName( fieldDef->idx() );
newWidgetInfo.showLabel = widgetDef->showLabel();
break;
}
case QgsAttributeEditorElement::AeTypeRelation:
{
const QgsAttributeEditorRelation *relDef = static_cast<const QgsAttributeEditorRelation *>( widgetDef );
QgsRelationWidgetWrapper *rww = new QgsRelationWidgetWrapper( mLayer, relDef->relation(), nullptr, this );
rww->setConfig( mLayer->editFormConfig().widgetConfig( relDef->relation().id() ) );
rww->setContext( context );
rww->setShowLabel( relDef->showLabel() );
rww->setShowLinkButton( relDef->showLinkButton() );
rww->setShowUnlinkButton( relDef->showUnlinkButton() );
QgsAttributeFormRelationEditorWidget *formWidget = new QgsAttributeFormRelationEditorWidget( rww, this );
formWidget->createSearchWidgetWrappers( mContext );
mWidgets.append( rww );
mFormWidgets.append( formWidget );
newWidgetInfo.widget = formWidget;
newWidgetInfo.labelText = QString();
newWidgetInfo.labelOnTop = true;
break;
}
case QgsAttributeEditorElement::AeTypeContainer:
{
const QgsAttributeEditorContainer *container = dynamic_cast<const QgsAttributeEditorContainer *>( widgetDef );
if ( !container )
break;
int columnCount = container->columnCount();
if ( columnCount <= 0 )
columnCount = 1;
QWidget *myContainer = nullptr;
if ( container->isGroupBox() )
{
QGroupBox *groupBox = new QGroupBox( parent );
if ( container->showLabel() )
groupBox->setTitle( container->name() );
myContainer = groupBox;
newWidgetInfo.widget = myContainer;
}
else
{
myContainer = new QWidget();
if ( context.formMode() != QgsAttributeEditorContext::Embed )
{
QgsScrollArea *scrollArea = new QgsScrollArea( parent );
scrollArea->setWidget( myContainer );
scrollArea->setWidgetResizable( true );
scrollArea->setFrameShape( QFrame::NoFrame );
newWidgetInfo.widget = scrollArea;
}
else
{
newWidgetInfo.widget = myContainer;
}
}
QGridLayout *gbLayout = new QGridLayout();
myContainer->setLayout( gbLayout );
int row = 0;
int column = 0;
QList<QgsAttributeEditorElement *> children = container->children();
Q_FOREACH ( QgsAttributeEditorElement *childDef, children )
{
WidgetInfo widgetInfo = createWidgetFromDef( childDef, myContainer, vl, context );
if ( childDef->type() == QgsAttributeEditorElement::AeTypeContainer )
{
QgsAttributeEditorContainer *containerDef = static_cast<QgsAttributeEditorContainer *>( childDef );
registerContainerInformation( new ContainerInformation( widgetInfo.widget, containerDef->visibilityExpression().data() ) );
}
if ( widgetInfo.labelText.isNull() )
{
gbLayout->addWidget( widgetInfo.widget, row, column, 1, 2 );
column += 2;
}
else
{
QLabel *mypLabel = new QLabel( widgetInfo.labelText );
mypLabel->setToolTip( QStringLiteral( "<b>%1</b><p>%2</p>" ).arg( widgetInfo.labelText, widgetInfo.hint ) );
if ( columnCount > 1 && !widgetInfo.labelOnTop )
{
mypLabel->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
}
mypLabel->setBuddy( widgetInfo.widget );
if ( widgetInfo.labelOnTop )
{
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
{
gbLayout->addWidget( mypLabel, row, column++ );
gbLayout->addWidget( widgetInfo.widget, row, column++ );
}
}
if ( column >= columnCount * 2 )
{
column = 0;
row += 1;
}
}
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;
break;
}
default:
QgsDebugMsg( "Unknown attribute editor widget type encountered..." );
break;
}
newWidgetInfo.showLabel = widgetDef->showLabel();
return newWidgetInfo;
}
void QgsAttributeForm::addWidgetWrapper( QgsEditorWidgetWrapper *eww )
{
Q_FOREACH ( QgsWidgetWrapper *ww, mWidgets )
{
QgsEditorWidgetWrapper *meww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( meww )
{
if ( meww->field() == eww->field() )
{
connect( meww, static_cast<void ( QgsEditorWidgetWrapper::* )( const QVariant & )>( &QgsEditorWidgetWrapper::valueChanged ), eww, &QgsEditorWidgetWrapper::setValue );
connect( eww, static_cast<void ( QgsEditorWidgetWrapper::* )( const QVariant & )>( &QgsEditorWidgetWrapper::valueChanged ), meww, &QgsEditorWidgetWrapper::setValue );
break;
}
}
}
mWidgets.append( eww );
}
void QgsAttributeForm::createWrappers()
{
QList<QWidget *> myWidgets = findChildren<QWidget *>();
const QList<QgsField> fields = mLayer->fields().toList();
Q_FOREACH ( QWidget *myWidget, myWidgets )
{
// 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
{
Q_FOREACH ( const QgsField &field, fields )
{
if ( field.name() == myWidget->objectName() )
{
int idx = mLayer->fields().lookupField( field.name() );
QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( mLayer, idx, myWidget, this, mContext );
addWidgetWrapper( eww );
}
}
}
}
}
void QgsAttributeForm::afterWidgetInit()
{
bool isFirstEww = true;
Q_FOREACH ( QgsWidgetWrapper *ww, mWidgets )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww )
{
if ( isFirstEww )
{
setFocusProxy( eww->widget() );
isFirstEww = false;
}
connect( eww, static_cast<void ( QgsEditorWidgetWrapper::* )( const QVariant & )>( &QgsEditorWidgetWrapper::valueChanged ), this, &QgsAttributeForm::onAttributeChanged );
connect( eww, &QgsEditorWidgetWrapper::constraintStatusChanged, this, &QgsAttributeForm::onConstraintStatusChanged );
}
}
}
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 SingleEditMode:
case IdentifyMode:
case AddFeatureMode:
case SearchMode:
case AggregateSearchMode:
break;
case MultiEditMode:
resetMultiEdit( true );
break;
}
}
void QgsAttributeForm::setMultiEditFeatureIds( const QgsFeatureIds &fids )
{
mIsSettingMultiEditFeatures = true;
mMultiEditFeatureIds = fids;
if ( fids.isEmpty() )
{
// no selected features
QMap< 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 );
Q_FOREACH ( int field, mixedValueFields )
{
if ( QgsAttributeFormEditorWidget *w = mFormEditorWidgets.value( field, nullptr ) )
{
w->initialize( firstFeature.attribute( field ), true );
}
}
QHash< int, QVariant >::const_iterator sharedValueIt = fieldSharedValues.constBegin();
for ( ; sharedValueIt != fieldSharedValues.constEnd(); ++sharedValueIt )
{
if ( QgsAttributeFormEditorWidget *w = mFormEditorWidgets.value( sharedValueIt.key(), nullptr ) )
{
w->initialize( sharedValueIt.value(), false );
}
}
mIsSettingMultiEditFeatures = false;
}
void QgsAttributeForm::setMessageBar( QgsMessageBar *messageBar )
{
if ( mOwnsMessageBar )
delete mMessageBar;
mOwnsMessageBar = false;
mMessageBar = messageBar;
}
QString QgsAttributeForm::aggregateFilter() const
{
if ( mMode != AggregateSearchMode )
{
Q_ASSERT( false );
}
QStringList filters;
for ( QgsAttributeFormWidget *widget : mFormWidgets )
{
QString filter = widget->currentFilterExpression();
if ( !filter.isNull() )
filters << '(' + filter + ')';
}
return filters.join( QStringLiteral( " AND " ) );
}
int QgsAttributeForm::messageTimeout()
{
QgsSettings settings;
return settings.value( QStringLiteral( "qgis/messageTimeout" ), 5 ).toInt();
}
void QgsAttributeForm::ContainerInformation::apply( QgsExpressionContext *expressionContext )
{
bool newVisibility = expression.evaluate( expressionContext ).toBool();
if ( newVisibility != isVisible )
{
if ( tabWidget )
{
tabWidget->setTabVisible( widget, newVisibility );
}
else
{
widget->setVisible( newVisibility );
}
isVisible = newVisibility;
}
}
void QgsAttributeForm::updateJoinedFields( const QgsEditorWidgetWrapper &eww )
{
QgsFeature formFeature;
QgsField field = eww.layer()->fields().field( eww.fieldIdx() );
QList<const QgsVectorLayerJoinInfo *> infos = eww.layer()->joinBuffer()->joinsWhereFieldIsId( field );
if ( infos.count() == 0 || !currentFormFeature( formFeature ) )
return;
const QString hint = tr( "No feature joined" );
Q_FOREACH ( const QgsVectorLayerJoinInfo *info, infos )
{
if ( !info->isDynamicFormEnabled() )
continue;
QgsFeature joinFeature = mLayer->joinBuffer()->joinedFeatureOf( info, formFeature );
mJoinedFeatures[info] = joinFeature;
if ( info->hasSubset() )
{
const QStringList subsetNames = QgsVectorLayerJoinInfo::joinFieldNamesSubset( *info );
Q_FOREACH ( const QString &field, subsetNames )
{
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
{
bool editable = false;
if ( mLayer->fields().fieldOrigin( fieldIndex ) == QgsFields::OriginJoin )
{
int srcFieldIndex;
const QgsVectorLayerJoinInfo *info = mLayer->joinBuffer()->joinForFieldIndex( fieldIndex, mLayer->fields(), srcFieldIndex );
if ( info && !info->hasUpsertOnEdit() && mMode == QgsAttributeForm::AddFeatureMode )
editable = false;
else if ( info && info->isEditable() && info->joinLayer()->isEditable() )
editable = fieldIsEditable( *( info->joinLayer() ), srcFieldIndex, mFeature.id() );
}
else
editable = fieldIsEditable( *mLayer, fieldIndex, mFeature.id() );
return editable;
}
bool QgsAttributeForm::fieldIsEditable( const QgsVectorLayer &layer, int fieldIndex, QgsFeatureId fid ) const
{
return !layer.editFormConfig().readOnly( fieldIndex ) &&
( ( layer.dataProvider() && layer.dataProvider()->capabilities() & QgsVectorDataProvider::ChangeAttributeValues ) || FID_IS_NEW( fid ) );
}
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() ) == QgsFields::OriginJoin )
{
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 == QgsAttributeForm::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();
}