/*************************************************************************** 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(); }