diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 4f5a5f04d6c..ada76e0af9f 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -311,6 +311,7 @@ %Include qgsfieldmodel.sip %Include qgsfieldproxymodel.sip %Include qgsfiledownloader.sip +%Include qgsfeaturefiltermodel.sip %Include qgsgeometryvalidator.sip %Include qgsgml.sip %Include qgsgmlschema.sip diff --git a/python/core/qgsfeaturefiltermodel.sip b/python/core/qgsfeaturefiltermodel.sip new file mode 100644 index 00000000000..6339929940c --- /dev/null +++ b/python/core/qgsfeaturefiltermodel.sip @@ -0,0 +1,118 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsfeaturefiltermodel.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsFeatureFilterModel : QAbstractItemModel +{ +%Docstring + Provides a list of features based on filter conditions. + Features are fetched asynchronously. +%End + +%TypeHeaderCode +#include "qgsfeaturefiltermodel.h" +%End + public: + enum Role + { + IdentifierValueRole, + ValueRole + }; + + QgsFeatureFilterModel( QObject *parent = 0 ); + ~QgsFeatureFilterModel(); + + QgsVectorLayer *sourceLayer() const; +%Docstring + :rtype: QgsVectorLayer +%End + void setSourceLayer( QgsVectorLayer *sourceLayer ); + + QString displayExpression() const; +%Docstring + :rtype: str +%End + void setDisplayExpression( const QString &displayExpression ); + + QString filterValue() const; +%Docstring + :rtype: str +%End + void setFilterValue( const QString &filterValue ); + + virtual QModelIndex index( int row, int column, const QModelIndex &parent ) const; + virtual QModelIndex parent( const QModelIndex &child ) const; + virtual int rowCount( const QModelIndex &parent ) const; + virtual int columnCount( const QModelIndex &parent ) const; + virtual QVariant data( const QModelIndex &index, int role ) const; + + QString filterExpression() const; +%Docstring + An additional filter expression to apply, next to the filterValue. + Can be used for spatial filtering etc. + :rtype: str +%End + + void setFilterExpression( const QString &filterExpression ); +%Docstring + An additional filter expression to apply, next to the filterValue. + Can be used for spatial filtering etc. +%End + + bool isLoading() const; +%Docstring + :rtype: bool +%End + + QString identifierField() const; +%Docstring + :rtype: str +%End + void setIdentifierField( const QString &identifierField ); + + QVariant extraIdentifierValue() const; +%Docstring + :rtype: QVariant +%End + void setExtraIdentifierValue( const QVariant &extraIdentifierValue ); + + int extraIdentifierValueIndex() const; +%Docstring + :rtype: int +%End + + bool extraValueDoesNotExist() const; +%Docstring + :rtype: bool +%End + + signals: + void sourceLayerChanged(); + void displayExpressionChanged(); + void filterValueChanged(); + void filterExpressionChanged(); + void isLoadingChanged(); + void identifierFieldChanged(); + void filterJobCompleted(); + void extraIdentifierValueChanged(); + void extraIdentifierValueIndexChanged( int index ); + void extraValueDoesNotExistChanged(); + void beginUpdate(); + void endUpdate(); + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsfeaturefiltermodel.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index 367cd6bfaff..eddb79140ca 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -108,6 +108,7 @@ %Include qgsfeatureselectiondlg.sip %Include qgsfieldcombobox.sip %Include qgsfieldexpressionwidget.sip +%Include qgsfeaturelistcombobox.sip %Include qgsfieldvalidator.sip %Include qgsfieldvalueslineedit.sip %Include qgsfilewidget.sip diff --git a/python/gui/qgsfeaturelistcombobox.sip b/python/gui/qgsfeaturelistcombobox.sip new file mode 100644 index 00000000000..d1532988243 --- /dev/null +++ b/python/gui/qgsfeaturelistcombobox.sip @@ -0,0 +1,104 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsfeaturelistcombobox.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsFeatureListComboBox : QComboBox +{ +%Docstring +************************************************************************* +qgsfieldlistcombobox.h - QgsFieldListComboBox + +--------------------- +begin : 10.3.2017 +copyright : (C) 2017 by Matthias Kuhn +email : matthias@opengis.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. * + * +************************************************************************** +%End + +%TypeHeaderCode +#include "qgsfeaturelistcombobox.h" +%End + public: + QgsFeatureListComboBox( QWidget *parent = 0 ); + + QgsVectorLayer *sourceLayer() const; +%Docstring + :rtype: QgsVectorLayer +%End + void setSourceLayer( QgsVectorLayer *sourceLayer ); + + QString displayExpression() const; +%Docstring + :rtype: str +%End + void setDisplayExpression( const QString &displayExpression ); + + QString filterExpression() const; +%Docstring + :rtype: str +%End + void setFilterExpression( const QString &filterExpression ); + + QVariant identifierValue() const; +%Docstring + :rtype: QVariant +%End + void setIdentifierValue( const QVariant &identifierValue ); + + QgsFeatureRequest currentFeatureRequest() const; +%Docstring + :rtype: QgsFeatureRequest +%End + + bool allowNull() const; +%Docstring + :rtype: bool +%End + void setAllowNull( bool allowNull ); + + QString identifierField() const; +%Docstring + :rtype: str +%End + void setIdentifierField( const QString &identifierField ); + + QModelIndex currentModelIndex() const; +%Docstring + :rtype: QModelIndex +%End + + virtual void focusOutEvent( QFocusEvent *event ); + + virtual void keyPressEvent( QKeyEvent *event ); + + signals: + void sourceLayerChanged(); + void displayExpressionChanged(); + void filterExpressionChanged(); + void identifierValueChanged(); + void identifierFieldChanged(); + void allowNullChanged(); + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsfeaturelistcombobox.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 2467f7eb662..472c8158ec8 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -175,6 +175,7 @@ SET(QGIS_CORE_SRCS qgsfeaturesink.cpp qgsfeaturesource.cpp qgsfeaturestore.cpp + qgsfeaturefiltermodel.cpp qgsfield.cpp qgsfieldconstraints.cpp qgsfieldformatter.cpp @@ -590,6 +591,8 @@ SET(QGIS_CORE_MOC_HDRS qgsfieldmodel.h qgsfieldproxymodel.h qgsfiledownloader.h + qgsfeaturefiltermodel.h + qgsfeaturefiltermodel_p.h qgsgeometryvalidator.h qgsgml.h qgsgmlschema.h diff --git a/src/core/qgsfeaturefiltermodel.cpp b/src/core/qgsfeaturefiltermodel.cpp new file mode 100644 index 00000000000..c54bd58f95e --- /dev/null +++ b/src/core/qgsfeaturefiltermodel.cpp @@ -0,0 +1,412 @@ +/*************************************************************************** + qgsfeaturefiltermodel.cpp - QgsFeatureFilterModel + + --------------------- + begin : 10.3.2017 + copyright : (C) 2017 by Matthias Kuhn + email : matthias@opengis.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 "qgsfeaturefiltermodel.h" +#include "qgsfeaturefiltermodel_p.h" + +QgsFeatureFilterModel::QgsFeatureFilterModel( QObject *parent ) + : QAbstractItemModel( parent ) + , mSourceLayer( nullptr ) +{ + mReloadTimer.setInterval( 100 ); + mReloadTimer.setSingleShot( true ); + connect( &mReloadTimer, &QTimer::timeout, this, &QgsFeatureFilterModel::scheduledReload ); + setExtraIdentifierValueUnguarded( QVariant() ); +} + +QgsFeatureFilterModel::~QgsFeatureFilterModel() +{ + if ( mGatherer ) + connect( mGatherer, &QgsFieldExpressionValuesGatherer::finished, mGatherer, &QgsFieldExpressionValuesGatherer::deleteLater ); +} + +QgsVectorLayer *QgsFeatureFilterModel::sourceLayer() const +{ + return mSourceLayer; +} + +void QgsFeatureFilterModel::setSourceLayer( QgsVectorLayer *sourceLayer ) +{ + if ( mSourceLayer == sourceLayer ) + return; + + mSourceLayer = sourceLayer; + reload(); + emit sourceLayerChanged(); +} + +QString QgsFeatureFilterModel::displayExpression() const +{ + return mDisplayExpression; +} + +void QgsFeatureFilterModel::setDisplayExpression( const QString &displayExpression ) +{ + if ( mDisplayExpression == displayExpression ) + return; + + mDisplayExpression = displayExpression; + reload(); + emit displayExpressionChanged(); +} + +QString QgsFeatureFilterModel::filterValue() const +{ + return mFilterValue; +} + +void QgsFeatureFilterModel::setFilterValue( const QString &filterValue ) +{ + if ( mFilterValue == filterValue ) + return; + + mFilterValue = filterValue; + reload(); + emit filterValueChanged(); +} + +QString QgsFeatureFilterModel::filterExpression() const +{ + return mFilterExpression; +} + +void QgsFeatureFilterModel::setFilterExpression( const QString &filterExpression ) +{ + if ( mFilterExpression == filterExpression ) + return; + + mFilterExpression = filterExpression; + reload(); + emit filterExpressionChanged(); +} + +bool QgsFeatureFilterModel::isLoading() const +{ + return mGatherer; +} + +QModelIndex QgsFeatureFilterModel::index( int row, int column, const QModelIndex &parent ) const +{ + Q_UNUSED( parent ) + return createIndex( row, column, nullptr ); +} + +QModelIndex QgsFeatureFilterModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +int QgsFeatureFilterModel::rowCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ); + + return mEntries.size(); +} + +int QgsFeatureFilterModel::columnCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ) + return 1; +} + +QVariant QgsFeatureFilterModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + case ValueRole: + return mEntries.value( index.row() ).value; + + case IdentifierValueRole: + return mEntries.value( index.row() ).identifierValue; + } + + return QVariant(); +} + +void QgsFeatureFilterModel::updateCompleter() +{ + emit beginUpdate(); + QVector entries = mGatherer->entries(); + + if ( mExtraIdentifierValueIndex == -1 ) + setExtraIdentifierValueUnguarded( QVariant() ); + + // Only reloading the current entry? + if ( mGatherer->data().toBool() ) + { + if ( !entries.isEmpty() ) + { + mEntries.replace( mExtraIdentifierValueIndex, entries.at( 0 ) ); + emit dataChanged( index( mExtraIdentifierValueIndex, 0, QModelIndex() ), index( mExtraIdentifierValueIndex, 0, QModelIndex() ) ); + mShouldReloadCurrentFeature = false; + setExtraValueDoesNotExist( false ); + } + else + { + setExtraValueDoesNotExist( true ); + } + + mShouldReloadCurrentFeature = false; + } + else + { + // We got strings for a filter selection + std::sort( entries.begin(), entries.end(), []( const Entry & a, const Entry & b ) { return a.value.localeAwareCompare( b.value ) < 0; } ); + + int newEntriesSize = entries.size(); + + // Find the index of the extra entry in the new list + int currentEntryInNewList = -1; + if ( mExtraIdentifierValueIndex != -1 ) + { + for ( int i = 0; i < newEntriesSize; ++i ) + { + if ( entries.at( i ).identifierValue == mExtraIdentifierValue ) + { + currentEntryInNewList = i; + mEntries.replace( mExtraIdentifierValueIndex, entries.at( i ) ); + emit dataChanged( index( mExtraIdentifierValueIndex, 0, QModelIndex() ), index( mExtraIdentifierValueIndex, 0, QModelIndex() ) ); + setExtraValueDoesNotExist( false ); + break; + } + } + } + else + { + Q_ASSERT_X( false, "QgsFeatureFilterModel::updateCompleter", "No extra identifier value generated. Should not get here." ); + } + + int firstRow = 0; + + // Move the extra entry to the first position + if ( mExtraIdentifierValueIndex != -1 ) + { + if ( mExtraIdentifierValueIndex != 0 ) + { + beginMoveRows( QModelIndex(), mExtraIdentifierValueIndex, mExtraIdentifierValueIndex, QModelIndex(), 0 ); + mEntries.move( mExtraIdentifierValueIndex, 0 ); + endMoveRows(); + } + firstRow = 1; + } + + // Remove all entries (except for extra entry if existant) + beginRemoveRows( QModelIndex(), firstRow, mEntries.size() - firstRow ); + mEntries.remove( firstRow, mEntries.size() - firstRow ); + endRemoveRows(); + + if ( currentEntryInNewList == -1 ) + { + beginInsertRows( QModelIndex(), 1, entries.size() + 1 ); + mEntries += entries; + endInsertRows(); + setExtraIdentifierValueIndex( 0 ); + } + else + { + if ( currentEntryInNewList != 0 ) + { + beginInsertRows( QModelIndex(), 0, currentEntryInNewList - 1 ); + mEntries = entries.mid( 0, currentEntryInNewList ); + endInsertRows(); + } + else + { + mEntries.replace( 0, entries.at( 0 ) ); + } + + emit dataChanged( index( currentEntryInNewList, 0, QModelIndex() ), index( currentEntryInNewList, 0, QModelIndex() ) ); + + beginInsertRows( QModelIndex(), currentEntryInNewList + 1, newEntriesSize - currentEntryInNewList - 1 ); + mEntries += entries.mid( currentEntryInNewList + 1 ); + endInsertRows(); + setExtraIdentifierValueIndex( currentEntryInNewList ); + } + + emit filterJobCompleted(); + } + emit endUpdate(); +} + +void QgsFeatureFilterModel::gathererThreadFinished() +{ + delete mGatherer; + mGatherer = nullptr; + emit isLoadingChanged(); +} + +void QgsFeatureFilterModel::scheduledReload() +{ + if ( !mSourceLayer ) + return; + + bool wasLoading = false; + + if ( mGatherer ) + { + // Send the gatherer thread to the graveyard: + // forget about it, tell it to stop and delete when finished + disconnect( mGatherer, &QgsFieldExpressionValuesGatherer::collectedValues, this, &QgsFeatureFilterModel::updateCompleter ); + disconnect( mGatherer, &QgsFieldExpressionValuesGatherer::finished, this, &QgsFeatureFilterModel::gathererThreadFinished ); + connect( mGatherer, &QgsFieldExpressionValuesGatherer::finished, mGatherer, &QgsFieldExpressionValuesGatherer::deleteLater ); + mGatherer->stop(); + wasLoading = true; + } + + QgsFeatureRequest request; + + if ( mShouldReloadCurrentFeature ) + { + request.setFilterExpression( QStringLiteral( "%1 = %2" ).arg( QgsExpression::quotedColumnRef( mIdentifierField ), QgsExpression::quotedValue( mExtraIdentifierValue ) ) ); + } + else + { + QString filterClause; + + if ( mFilterValue.isEmpty() && !mFilterExpression.isEmpty() ) + filterClause = mFilterExpression; + else if ( mFilterExpression.isEmpty() && !mFilterValue.isEmpty() ) + filterClause = QStringLiteral( "(%1) ILIKE '\%%2\%'" ).arg( mDisplayExpression, mFilterValue ); + else if ( !mFilterExpression.isEmpty() && !mFilterValue.isEmpty() ) + filterClause = QStringLiteral( "(%1) AND ((%2) ILIKE '\%%3\%')" ).arg( mFilterExpression, mDisplayExpression, mFilterValue ); + + request.setFilterExpression( filterClause ); + } + + request.setLimit( 100 ); + + mGatherer = new QgsFieldExpressionValuesGatherer( mSourceLayer, mDisplayExpression, mIdentifierField, request ); + mGatherer->setData( mShouldReloadCurrentFeature ); + + connect( mGatherer, &QgsFieldExpressionValuesGatherer::collectedValues, this, &QgsFeatureFilterModel::updateCompleter ); + connect( mGatherer, &QgsFieldExpressionValuesGatherer::finished, this, &QgsFeatureFilterModel::gathererThreadFinished ); + + mGatherer->start(); + if ( !wasLoading ) + emit isLoadingChanged(); +} + +void QgsFeatureFilterModel::setExtraIdentifierValueIndex( int index ) +{ + if ( mExtraIdentifierValueIndex == index ) + return; + + mExtraIdentifierValueIndex = index; + emit extraIdentifierValueIndexChanged( index ); +} + +void QgsFeatureFilterModel::reloadCurrentFeature() +{ + mShouldReloadCurrentFeature = true; + mReloadTimer.start(); +} + +void QgsFeatureFilterModel::setExtraIdentifierValueUnguarded( const QVariant &extraIdentifierValue ) +{ + const QVector entries = mEntries; + + int index = 0; + for ( const Entry &entry : entries ) + { + if ( entry.identifierValue == extraIdentifierValue ) + { + setExtraIdentifierValueIndex( index ); + break; + } + + index++; + } + + // Value not found in current entries + if ( mExtraIdentifierValueIndex != index ) + { + beginInsertRows( QModelIndex(), 0, 0 ); + mEntries.prepend( Entry( extraIdentifierValue, QStringLiteral( "(%1)" ).arg( extraIdentifierValue.toString() ) ) ); + endInsertRows(); + setExtraIdentifierValueIndex( 0 ); + + reloadCurrentFeature(); + } +} + +bool QgsFeatureFilterModel::extraValueDoesNotExist() const +{ + return mExtraValueDoesNotExist; +} + +void QgsFeatureFilterModel::setExtraValueDoesNotExist( bool extraValueDoesNotExist ) +{ + if ( mExtraValueDoesNotExist == extraValueDoesNotExist ) + return; + + mExtraValueDoesNotExist = extraValueDoesNotExist; + emit extraValueDoesNotExistChanged(); +} + +int QgsFeatureFilterModel::extraIdentifierValueIndex() const +{ + return mExtraIdentifierValueIndex; +} + +QString QgsFeatureFilterModel::identifierField() const +{ + return mIdentifierField; +} + +void QgsFeatureFilterModel::setIdentifierField( const QString &identifierField ) +{ + if ( mIdentifierField == identifierField ) + return; + + mIdentifierField = identifierField; + emit identifierFieldChanged(); +} + +void QgsFeatureFilterModel::reload() +{ + mReloadTimer.start(); +} + +QVariant QgsFeatureFilterModel::extraIdentifierValue() const +{ + return mExtraIdentifierValue; +} + +void QgsFeatureFilterModel::setExtraIdentifierValue( const QVariant &extraIdentifierValue ) +{ + if ( extraIdentifierValue == mExtraIdentifierValue && extraIdentifierValue.isNull() == mExtraIdentifierValue.isNull() ) + return; + + setExtraIdentifierValueUnguarded( extraIdentifierValue ); + + mExtraIdentifierValue = extraIdentifierValue; + emit extraIdentifierValueChanged(); +} + +QVariant QgsFieldExpressionValuesGatherer::data() const +{ + return mData; +} + +void QgsFieldExpressionValuesGatherer::setData( const QVariant &data ) +{ + mData = data; +} diff --git a/src/core/qgsfeaturefiltermodel.h b/src/core/qgsfeaturefiltermodel.h new file mode 100644 index 00000000000..516dcf21de9 --- /dev/null +++ b/src/core/qgsfeaturefiltermodel.h @@ -0,0 +1,163 @@ +/*************************************************************************** + qgsfeaturefiltermodel.h - QgsFeatureFilterModel + + --------------------- + begin : 10.3.2017 + copyright : (C) 2017 by Matthias Kuhn + email : matthias@opengis.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. * + * * + ***************************************************************************/ +#ifndef QGSFEATUREFILTERMODEL_H +#define QGSFEATUREFILTERMODEL_H + +#include + +#include "qgsvectorlayer.h" + +class QgsFieldExpressionValuesGatherer; + +/** + * Provides a list of features based on filter conditions. + * Features are fetched asynchronously. + */ +class CORE_EXPORT QgsFeatureFilterModel : public QAbstractItemModel +{ + Q_OBJECT + + Q_PROPERTY( QgsVectorLayer *sourceLayer READ sourceLayer WRITE setSourceLayer NOTIFY sourceLayerChanged ) + Q_PROPERTY( QString displayExpression READ displayExpression WRITE setDisplayExpression NOTIFY displayExpressionChanged ) + Q_PROPERTY( QString filterValue READ filterValue WRITE setFilterValue NOTIFY filterValueChanged ) + Q_PROPERTY( QString filterExpression READ filterExpression WRITE setFilterExpression NOTIFY filterExpressionChanged ) + Q_PROPERTY( bool isLoading READ isLoading NOTIFY isLoadingChanged ) + + /** + * A field of sourceLayer that is unique and should be used to identify features. + * Normally the primary key field. + * Needs to match the identifierValue. + */ + Q_PROPERTY( QString identifierField READ identifierField WRITE setIdentifierField NOTIFY identifierFieldChanged ) + + /** + * The value that identifies the current feature. + */ + Q_PROPERTY( QVariant extraIdentifierValue READ extraIdentifierValue WRITE setExtraIdentifierValue NOTIFY extraIdentifierValueChanged ) + + Q_PROPERTY( int extraIdentifierValueIndex READ extraIdentifierValueIndex NOTIFY extraIdentifierValueIndexChanged ) + + public: + enum Role + { + IdentifierValueRole = Qt::UserRole, + ValueRole + }; + + QgsFeatureFilterModel( QObject *parent = nullptr ); + ~QgsFeatureFilterModel(); + + QgsVectorLayer *sourceLayer() const; + void setSourceLayer( QgsVectorLayer *sourceLayer ); + + QString displayExpression() const; + void setDisplayExpression( const QString &displayExpression ); + + QString filterValue() const; + void setFilterValue( const QString &filterValue ); + + virtual QModelIndex index( int row, int column, const QModelIndex &parent ) const override; + virtual QModelIndex parent( const QModelIndex &child ) const override; + virtual int rowCount( const QModelIndex &parent ) const override; + virtual int columnCount( const QModelIndex &parent ) const override; + virtual QVariant data( const QModelIndex &index, int role ) const override; + + /** + * An additional filter expression to apply, next to the filterValue. + * Can be used for spatial filtering etc. + */ + QString filterExpression() const; + + /** + * An additional filter expression to apply, next to the filterValue. + * Can be used for spatial filtering etc. + */ + void setFilterExpression( const QString &filterExpression ); + + bool isLoading() const; + + QString identifierField() const; + void setIdentifierField( const QString &identifierField ); + + QVariant extraIdentifierValue() const; + void setExtraIdentifierValue( const QVariant &extraIdentifierValue ); + + int extraIdentifierValueIndex() const; + + bool extraValueDoesNotExist() const; + + signals: + void sourceLayerChanged(); + void displayExpressionChanged(); + void filterValueChanged(); + void filterExpressionChanged(); + void isLoadingChanged(); + void identifierFieldChanged(); + void filterJobCompleted(); + void extraIdentifierValueChanged(); + void extraIdentifierValueIndexChanged( int index ); + void extraValueDoesNotExistChanged(); + void beginUpdate(); + void endUpdate(); + + private slots: + void updateCompleter(); + void gathererThreadFinished(); + void scheduledReload(); + + private: + void setExtraIdentifierValueIndex( int index ); + void setExtraValueDoesNotExist( bool extraValueDoesNotExist ); + void reload(); + void reloadCurrentFeature(); + void setExtraIdentifierValueUnguarded( const QVariant &extraIdentifierValue ); + struct Entry + { + Entry() + {} + + Entry( QVariant _identifierValue, const QString &_value ) + : identifierValue( _identifierValue ) + , value( _value ) + {} + + QVariant identifierValue; + QString value; + + bool operator()( const Entry &lhs, const Entry &rhs ) const; + }; + + QgsVectorLayer *mSourceLayer; + QString mDisplayExpression; + QString mFilterValue; + QString mFilterExpression; + + QVector mEntries; + QgsFieldExpressionValuesGatherer *mGatherer = nullptr; + QTimer mReloadTimer; + bool mShouldReloadCurrentFeature; + bool mExtraValueDoesNotExist = false; + + QString mIdentifierField; + + QVariant mExtraIdentifierValue; + + int mExtraIdentifierValueIndex = -1; + + friend class QgsFieldExpressionValuesGatherer; +}; + +#endif // QGSFEATUREFILTERMODEL_H diff --git a/src/core/qgsfeaturefiltermodel_p.h b/src/core/qgsfeaturefiltermodel_p.h new file mode 100644 index 00000000000..2de9a769859 --- /dev/null +++ b/src/core/qgsfeaturefiltermodel_p.h @@ -0,0 +1,131 @@ +/*************************************************************************** + qgsfeaturefiltermodel_p - QgsFieldExpressionValuesGatherer + + --------------------- + begin : 10.3.2017 + copyright : (C) 2017 by Matthias Kuhn + email : matthias@opengis.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. * + * * + ***************************************************************************/ +#ifndef QGSFEATUREFILTERMODEL_P_H +#define QGSFEATUREFILTERMODEL_P_H + +#include +#include "qgsvectorlayer.h" +#include "qgsfeaturefiltermodel.h" +#include "qgslogger.h" +#include "qgsvectorlayerfeatureiterator.h" + +#define SIP_NO_FILE + +// just internal guff - definitely not for exposing to public API! +///@cond PRIVATE + +/** + * \class QgsFieldExpressionValuesGatherer + * Gathers features with substring matching on an expression. + * + * \since QGIS 3.0 + */ +class QgsFieldExpressionValuesGatherer: public QThread +{ + Q_OBJECT + + public: + QgsFieldExpressionValuesGatherer( QgsVectorLayer *layer, const QString &displayExpression, const QString &identifierField, const QgsFeatureRequest &request = QgsFeatureRequest() ) + : mSource( new QgsVectorLayerFeatureSource( layer ) ) + , mDisplayExpression( displayExpression ) + , mRequest( request ) + , mWasCanceled( false ) + , mIdentifierField( identifierField ) + { + } + + ~QgsFieldExpressionValuesGatherer() + { + } + + virtual void run() override + { + mWasCanceled = false; + + mIterator = mSource->getFeatures( mRequest ); + + QgsDebugMsg( QStringLiteral( "New gatherer: %1" ).arg( mRequest.filterExpression()->expression() ) ); + mDisplayExpression.prepare( &mExpressionContext ); + + QgsFeature feat; + int attribute = mSource->fields().indexOf( mIdentifierField ); + + while ( mIterator.nextFeature( feat ) ) + { + mExpressionContext.setFeature( feat ); + mEntries.append( QgsFeatureFilterModel::Entry( feat.attribute( attribute ), mDisplayExpression.evaluate( &mExpressionContext ).toString() ) ); + + if ( mWasCanceled ) + return; + } + + emit collectedValues(); + } + + //! Informs the gatherer to immediately stop collecting values + void stop() + { + mWasCanceled = true; + } + + //! Returns true if collection was canceled before completion + bool wasCanceled() const { return mWasCanceled; } + + QVector entries() const + { + return mEntries; + } + + QgsFeatureRequest request() const + { + return mRequest; + } + + /** + * Internal data, use for whatever you want. Defaults to -1. + */ + QVariant data() const; + + /** + * Internal data, use for whatever you want. Defaults to -1. + */ + void setData( const QVariant &data ); // TODO: Do we still need this??? + + signals: + + /** + * Emitted when values have been collected + * @param values list of unique matching string values + */ + void collectedValues(); + + private: + + std::unique_ptr mSource; + QgsExpression mDisplayExpression; + QgsExpressionContext mExpressionContext; + QgsFeatureRequest mRequest; + QgsFeatureIterator mIterator; + bool mWasCanceled; + QVector mEntries; + QString mIdentifierField; + QVariant mData; +}; + +///@endcond + + +#endif // QGSFEATUREFILTERMODEL_P_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 817223555f1..803433561c8 100755 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -241,6 +241,7 @@ SET(QGIS_GUI_SRCS qgsfeatureselectiondlg.cpp qgsfieldcombobox.cpp qgsfieldexpressionwidget.cpp + qgsfeaturelistcombobox.cpp qgsfieldvalidator.cpp qgsfieldvalueslineedit.cpp qgsfilewidget.cpp @@ -412,6 +413,7 @@ SET(QGIS_GUI_MOC_HDRS qgsfeatureselectiondlg.h qgsfieldcombobox.h qgsfieldexpressionwidget.h + qgsfeaturelistcombobox.h qgsfieldvalidator.h qgsfieldvalueslineedit.h qgsfilewidget.h diff --git a/src/gui/qgsfeaturelistcombobox.cpp b/src/gui/qgsfeaturelistcombobox.cpp new file mode 100644 index 00000000000..d4df817a9ac --- /dev/null +++ b/src/gui/qgsfeaturelistcombobox.cpp @@ -0,0 +1,217 @@ +/*************************************************************************** + qgsfieldlistcombobox.cpp - QgsFieldListComboBox + + --------------------- + begin : 10.3.2017 + copyright : (C) 2017 by Matthias Kuhn + email : matthias@opengis.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 "qgsfeaturelistcombobox.h" + +#include "qgsfeaturefiltermodel.h" +#include "qgsanimatedicon.h" +#include "qgsfilterlineedit.h" +#include "qgslogger.h" + +#include +#include +#include + +QgsFeatureListComboBox::QgsFeatureListComboBox( QWidget *parent ) + : QComboBox( parent ) + , mModel( new QgsFeatureFilterModel( this ) ) + , mCompleter( new QCompleter( mModel ) ) +{ + mCompleter->setCaseSensitivity( Qt::CaseInsensitive ); + mCompleter->setFilterMode( Qt::MatchContains ); + setCompleter( mCompleter ); + mCompleter->setWidget( this ); + connect( mModel, &QgsFeatureFilterModel::sourceLayerChanged, this, &QgsFeatureListComboBox::sourceLayerChanged ); + connect( mModel, &QgsFeatureFilterModel::displayExpressionChanged, this, &QgsFeatureListComboBox::displayExpressionChanged ); + connect( mModel, &QgsFeatureFilterModel::filterExpressionChanged, this, &QgsFeatureListComboBox::filterExpressionChanged ); + connect( mModel, &QgsFeatureFilterModel::isLoadingChanged, this, &QgsFeatureListComboBox::onLoadingChanged ); + connect( mModel, &QgsFeatureFilterModel::filterJobCompleted, this, &QgsFeatureListComboBox::onFilterUpdateCompleted ); + connect( mModel, &QgsFeatureFilterModel::extraIdentifierValueChanged, this, &QgsFeatureListComboBox::identifierValueChanged ); + connect( mModel, &QgsFeatureFilterModel::extraIdentifierValueIndexChanged, this, &QgsFeatureListComboBox::setCurrentIndex ); + connect( mModel, &QgsFeatureFilterModel::identifierFieldChanged, this, &QgsFeatureListComboBox::identifierFieldChanged ); + connect( mCompleter, static_cast( &QCompleter::highlighted ), this, &QgsFeatureListComboBox::onItemSelected ); + connect( mCompleter, static_cast( &QCompleter::activated ), this, &QgsFeatureListComboBox::onActivated ); + connect( mModel, &QgsFeatureFilterModel::beginUpdate, this, &QgsFeatureListComboBox::storeLineEditState ); + connect( mModel, &QgsFeatureFilterModel::endUpdate, this, &QgsFeatureListComboBox::restoreLineEditState ); + + connect( this, static_cast( &QgsFeatureListComboBox::currentIndexChanged ), this, &QgsFeatureListComboBox::onCurrentIndexChanged ); + + mLineEdit = new QgsFilterLineEdit(); + setEditable( true ); + setLineEdit( mLineEdit ); + setModel( mModel ); + + connect( mLineEdit, &QgsFilterLineEdit::textEdited, this, &QgsFeatureListComboBox::onCurrentTextChanged ); + + connect( mLineEdit, &QgsFilterLineEdit::textChanged, this, []( const QString & text ) + { + QgsDebugMsg( QStringLiteral( "Edit text changed to %1" ).arg( text ) ); + } ); +} + +QgsVectorLayer *QgsFeatureListComboBox::sourceLayer() const +{ + return mModel->sourceLayer(); +} + +void QgsFeatureListComboBox::setSourceLayer( QgsVectorLayer *sourceLayer ) +{ + mModel->setSourceLayer( sourceLayer ); +} + +QString QgsFeatureListComboBox::displayExpression() const +{ + return mModel->displayExpression(); +} + +void QgsFeatureListComboBox::setDisplayExpression( const QString &expression ) +{ + mModel->setDisplayExpression( expression ); +} + +void QgsFeatureListComboBox::onCurrentTextChanged( const QString &text ) +{ + mPopupRequested = true; + mModel->setFilterValue( text ); +} + +void QgsFeatureListComboBox::onFilterUpdateCompleted() +{ + if ( mPopupRequested ) + mCompleter->complete(); + + mPopupRequested = false; +} + +void QgsFeatureListComboBox::onLoadingChanged() +{ + mLineEdit->setShowSpinner( mModel->isLoading() ); +} + +void QgsFeatureListComboBox::onItemSelected( const QModelIndex &index ) +{ + setCurrentIndex( index.row() ); +} + +void QgsFeatureListComboBox::onCurrentIndexChanged( int i ) +{ + QModelIndex modelIndex = mModel->index( i, 0, QModelIndex() ); + mModel->setExtraIdentifierValue( mModel->data( modelIndex, QgsFeatureFilterModel::IdentifierValueRole ) ); + mLineEdit->setText( mModel->data( modelIndex, QgsFeatureFilterModel::ValueRole ).toString() ); +} + +void QgsFeatureListComboBox::onActivated( QModelIndex modelIndex ) +{ + setIdentifierValue( mModel->data( modelIndex, QgsFeatureFilterModel::IdentifierValueRole ) ); + QgsDebugMsg( QStringLiteral( "Activated index" ) ); + QgsDebugMsg( QStringLiteral( "%1 %2" ).arg( QString::number( modelIndex.row() ), mModel->data( modelIndex, QgsFeatureFilterModel::ValueRole ).toString() ) ); + mLineEdit->setText( mModel->data( modelIndex, QgsFeatureFilterModel::ValueRole ).toString() ); +} + +void QgsFeatureListComboBox::storeLineEditState() +{ + mLineEditState.store( mLineEdit ); +} + +void QgsFeatureListComboBox::restoreLineEditState() +{ + mLineEditState.restore( mLineEdit ); +} + +QString QgsFeatureListComboBox::identifierField() const +{ + return mModel->identifierField(); +} + +void QgsFeatureListComboBox::setIdentifierField( const QString &identifierField ) +{ + mModel->setIdentifierField( identifierField ); +} + +QModelIndex QgsFeatureListComboBox::currentModelIndex() const +{ + return mModel->index( mModel->extraIdentifierValueIndex(), 0, QModelIndex() ); +} + +void QgsFeatureListComboBox::focusOutEvent( QFocusEvent *event ) +{ + Q_UNUSED( event ) + QComboBox::focusOutEvent( event ); + mLineEdit->setText( mModel->data( currentModelIndex(), QgsFeatureFilterModel::ValueRole ).toString() ); +} + +void QgsFeatureListComboBox::keyPressEvent( QKeyEvent *event ) +{ + if ( event->key() == Qt::Key_Escape ) + { + mLineEdit->setText( mModel->data( currentModelIndex(), QgsFeatureFilterModel::ValueRole ).toString() ); + } + QComboBox::keyReleaseEvent( event ); +} + +bool QgsFeatureListComboBox::allowNull() const +{ + return mAllowNull; +} + +void QgsFeatureListComboBox::setAllowNull( bool allowNull ) +{ + if ( mAllowNull == allowNull ) + return; + + mAllowNull = allowNull; + emit allowNullChanged(); +} + +QVariant QgsFeatureListComboBox::identifierValue() const +{ + return mModel->extraIdentifierValue(); +} + +void QgsFeatureListComboBox::setIdentifierValue( const QVariant &identifierValue ) +{ + mModel->setExtraIdentifierValue( identifierValue ); +} + +QgsFeatureRequest QgsFeatureListComboBox::currentFeatureRequest() const +{ + return QgsFeatureRequest().setFilterExpression( QStringLiteral( "%1 = %2" ).arg( QgsExpression::quotedColumnRef( mModel->identifierField() ), QgsExpression::quotedValue( mModel->extraIdentifierValue() ) ) ); +} + +QString QgsFeatureListComboBox::filterExpression() const +{ + return mModel->filterExpression(); +} + +void QgsFeatureListComboBox::setFilterExpression( const QString &filterExpression ) +{ + mModel->setFilterExpression( filterExpression ); +} + +void QgsFeatureListComboBox::LineEditState::store( QLineEdit *lineEdit ) +{ + text = lineEdit->text(); + selectionStart = lineEdit->selectionStart(); + selectionLength = lineEdit->selectedText().length(); + cursorPosition = lineEdit->cursorPosition(); + +} + +void QgsFeatureListComboBox::LineEditState::restore( QLineEdit *lineEdit ) const +{ + lineEdit->setText( text ); + lineEdit->setCursorPosition( cursorPosition ); + lineEdit->setSelection( selectionStart, selectionLength ); +} diff --git a/src/gui/qgsfeaturelistcombobox.h b/src/gui/qgsfeaturelistcombobox.h new file mode 100644 index 00000000000..e334c2bf930 --- /dev/null +++ b/src/gui/qgsfeaturelistcombobox.h @@ -0,0 +1,110 @@ +/*************************************************************************** + qgsfieldlistcombobox.h - QgsFieldListComboBox + + --------------------- + begin : 10.3.2017 + copyright : (C) 2017 by Matthias Kuhn + email : matthias@opengis.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. * + * * + ***************************************************************************/ +#ifndef QGSFIELDLISTCOMBOBOX_H +#define QGSFIELDLISTCOMBOBOX_H + +#include + +#include "qgsfeature.h" +#include "qgsfeaturerequest.h" +#include "qgis_gui.h" + +class QgsVectorLayer; +class QgsFeatureFilterModel; +class QgsAnimatedIcon; +class QgsFilterLineEdit; + +class GUI_EXPORT QgsFeatureListComboBox : public QComboBox +{ + Q_OBJECT + + Q_PROPERTY( QgsVectorLayer *sourceLayer READ sourceLayer WRITE setSourceLayer NOTIFY sourceLayerChanged ) + Q_PROPERTY( QString displayExpression READ displayExpression WRITE setDisplayExpression NOTIFY displayExpressionChanged ) + Q_PROPERTY( QString filterExpression READ filterExpression WRITE setFilterExpression NOTIFY filterExpressionChanged ) + Q_PROPERTY( QVariant identifierValue READ identifierValue WRITE setIdentifierValue NOTIFY identifierValueChanged ) + Q_PROPERTY( QString identifierField READ identifierField WRITE setIdentifierField NOTIFY identifierFieldChanged ) + Q_PROPERTY( bool allowNull READ allowNull WRITE setAllowNull NOTIFY allowNullChanged ) + + public: + QgsFeatureListComboBox( QWidget *parent = nullptr ); + + QgsVectorLayer *sourceLayer() const; + void setSourceLayer( QgsVectorLayer *sourceLayer ); + + QString displayExpression() const; + void setDisplayExpression( const QString &displayExpression ); + + QString filterExpression() const; + void setFilterExpression( const QString &filterExpression ); + + QVariant identifierValue() const; + void setIdentifierValue( const QVariant &identifierValue ); + + QgsFeatureRequest currentFeatureRequest() const; + + bool allowNull() const; + void setAllowNull( bool allowNull ); + + QString identifierField() const; + void setIdentifierField( const QString &identifierField ); + + QModelIndex currentModelIndex() const; + + virtual void focusOutEvent( QFocusEvent *event ) override; + + virtual void keyPressEvent( QKeyEvent *event ) override; + + signals: + void sourceLayerChanged(); + void displayExpressionChanged(); + void filterExpressionChanged(); + void identifierValueChanged(); + void identifierFieldChanged(); + void allowNullChanged(); + + private slots: + void onCurrentTextChanged( const QString &text ); + void onFilterUpdateCompleted(); + void onLoadingChanged(); + void onItemSelected( const QModelIndex &index ); + void onCurrentIndexChanged( int i ); + void onActivated( QModelIndex index ); + void storeLineEditState(); + void restoreLineEditState(); + + private: + struct LineEditState + { + void store( QLineEdit *lineEdit ); + void restore( QLineEdit *lineEdit ) const; + + QString text; + int selectionStart; + int selectionLength; + int cursorPosition; + }; + + QgsFeatureFilterModel *mModel; + QCompleter *mCompleter; + QString mDisplayExpression; + QgsFilterLineEdit *mLineEdit; + bool mAllowNull = true; + bool mPopupRequested = false; + bool mIsCurrentlyEdited = false; + LineEditState mLineEditState; +}; + +#endif // QGSFIELDLISTCOMBOBOX_H diff --git a/src/gui/qgsfeaturelistcombobox_p.h b/src/gui/qgsfeaturelistcombobox_p.h new file mode 100644 index 00000000000..6bc2f03936f --- /dev/null +++ b/src/gui/qgsfeaturelistcombobox_p.h @@ -0,0 +1,19 @@ +/*************************************************************************** + qgsfeaturelistcombobox_p.h + + --------------------- + begin : 24.10.2017 + copyright : (C) 2017 by Matthias Kuhn + email : matthias@opengis.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. * + * * + ***************************************************************************/ +#ifndef QGSFEATURELISTCOMBOBOX_P_H +#define QGSFEATURELISTCOMBOBOX_P_H + +#endif // QGSFEATURELISTCOMBOBOX_P_H