From c14ac23541edf46d22abff5559a1cb086ef01619 Mon Sep 17 00:00:00 2001 From: uclaros Date: Thu, 4 Jun 2026 18:15:11 +0300 Subject: [PATCH 1/5] Rework cancellation logic for LayerFeaturesModel searches --- app/layerfeaturesmodel.cpp | 90 +++++++++++++++++++++++++------------- app/layerfeaturesmodel.h | 18 +++++--- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/app/layerfeaturesmodel.cpp b/app/layerfeaturesmodel.cpp index 10eab35d8..0ec29454b 100644 --- a/app/layerfeaturesmodel.cpp +++ b/app/layerfeaturesmodel.cpp @@ -23,14 +23,15 @@ LayerFeaturesModel::LayerFeaturesModel( QObject *parent ) : FeaturesModel( parent ), mLayer( nullptr ) { - mFeedback = std::make_unique(); - connect( &mSearchResultWatcher, &QFutureWatcher::finished, this, &LayerFeaturesModel::onFutureFinished ); } LayerFeaturesModel::~LayerFeaturesModel() { // cancel any long running request - mFeedback->cancel(); + cancelPendingRequests(); + + for ( auto w : std::as_const( mSearchResultWatchers ) ) + w->waitForFinished(); } QVariant LayerFeaturesModel::data( const QModelIndex &index, int role ) const @@ -73,6 +74,8 @@ void LayerFeaturesModel::populate() { if ( mLayer ) { + cancelPendingRequests(); + mFetchingResults = true; emit fetchingResultsChanged( mFetchingResults ); beginResetModel(); @@ -82,26 +85,31 @@ void LayerFeaturesModel::populate() QgsFeatureRequest req; setupFeatureRequest( req ); - req.setFeedback( mFeedback.get() ); + const int searchId = mNextSearchId.fetchAndAddOrdered( 1 ); + QgsFeedback *feedback = new QgsFeedback( this ); + mFeedbacks[ searchId ] = feedback; + req.setFeedback( feedback ); + + QFutureWatcher *watcher = new QFutureWatcher( this ); + mSearchResultWatchers[ searchId ] = watcher; + connect( watcher, &QFutureWatcher::finished, this, &LayerFeaturesModel::onFutureFinished ); - int searchId = mNextSearchId.fetchAndAddOrdered( 1 ); QgsVectorLayerFeatureSource *source = new QgsVectorLayerFeatureSource( mLayer ); - mSearchResultWatcher.setFuture( QtConcurrent::run( &LayerFeaturesModel::fetchFeatures, this, source, req, searchId ) ); + watcher->setFuture( QtConcurrent::run( &LayerFeaturesModel::fetchFeatures, source, req, searchId ) ); } } -QgsFeatureList LayerFeaturesModel::fetchFeatures( QgsVectorLayerFeatureSource *source, QgsFeatureRequest req, int searchId ) +LayerFeaturesModel::SearchResultData LayerFeaturesModel::fetchFeatures( QgsVectorLayerFeatureSource *source, const QgsFeatureRequest &req, int searchId ) { std::unique_ptr fs( source ); QgsFeatureList fl; // a search might have been queued if no threads were available in the pool, so we also // check if canceled before we start as the first iteration can potentially be slow - bool canceled = searchId + 1 != mNextSearchId.loadAcquire(); - if ( canceled ) + if ( req.feedback()->isCanceled() ) { - qDebug() << QString( "Search (%1) was cancelled before it started!" ).arg( searchId ); - return fl; + qDebug() << QStringLiteral( "Search (%1) was cancelled before it started!" ).arg( searchId ); + return { searchId, fl }; } QElapsedTimer t; @@ -111,12 +119,6 @@ QgsFeatureList LayerFeaturesModel::fetchFeatures( QgsVectorLayerFeatureSource *s while ( it.nextFeature( f ) ) { - if ( searchId + 1 != mNextSearchId.loadAcquire() ) - { - canceled = true; - break; - } - if ( FID_IS_NEW( f.id() ) || FID_IS_NULL( f.id() ) ) { continue; // ignore uncommited features @@ -125,27 +127,44 @@ QgsFeatureList LayerFeaturesModel::fetchFeatures( QgsVectorLayerFeatureSource *s fl.append( f ); } - canceled = canceled || ( req.feedback() && req.feedback()->isCanceled() ); + const bool canceled = req.feedback()->isCanceled(); - qDebug() << QString( "Search (%1) %2 after %3ms, results: %4" ).arg( searchId ).arg( canceled ? "was canceled" : "completed" ).arg( t.elapsed() ).arg( fl.count() ); - return fl; + qDebug() << QStringLiteral( "Search (%1) %2 after %3ms, results: %4" ).arg( searchId ).arg( canceled ? QStringLiteral( "was canceled" ) : QStringLiteral( "completed" ) ).arg( t.elapsed() ).arg( fl.count() ); + return { searchId, fl }; } void LayerFeaturesModel::onFutureFinished() { - QFutureWatcher *watcher = static_cast< QFutureWatcher *>( sender() ); - const QgsFeatureList features = watcher->future().result(); - beginResetModel(); - mFeatures.clear(); - for ( const auto &f : features ) + QFutureWatcher *watcher = static_cast< QFutureWatcher *>( sender() ); + const SearchResultData data = watcher->future().result(); + + mSearchResultWatchers.remove( data.searchId ); + watcher->deleteLater(); + + QgsFeedback *feedback = mFeedbacks.take( data.searchId ); + feedback->deleteLater(); + + // We ignore the results of cancelled requests + if ( !feedback->isCanceled() ) + { + const QgsFeatureList features = data.features; + beginResetModel(); + mFeatures.clear(); + for ( const auto &f : features ) + { + mFeatures.emplaceBack( f, mLayer ); + } + emit layerFeaturesCountChanged( layerFeaturesCount() ); + emit countChanged( rowCount() ); + endResetModel(); + } + + // Only fire signal if that was the latest request + if ( data.searchId + 1 == mNextSearchId.loadAcquire() ) { - mFeatures << FeatureLayerPair( f, mLayer ); + mFetchingResults = false; + emit fetchingResultsChanged( mFetchingResults ); } - emit layerFeaturesCountChanged( layerFeaturesCount() ); - emit countChanged( rowCount() ); - endResetModel(); - mFetchingResults = false; - emit fetchingResultsChanged( mFetchingResults ); } QString LayerFeaturesModel::searchResultPair( const FeatureLayerPair &pair ) const @@ -179,6 +198,12 @@ QString LayerFeaturesModel::searchResultPair( const FeatureLayerPair &pair ) con return foundPairs.join( ", " ); } +void LayerFeaturesModel::cancelPendingRequests() +{ + for ( auto fb : std::as_const( mFeedbacks ) ) + fb->cancel(); +} + QString LayerFeaturesModel::buildSearchExpression() { if ( mSearchExpression.isEmpty() || !mLayer ) @@ -256,6 +281,7 @@ int LayerFeaturesModel::layerFeaturesCount() const void LayerFeaturesModel::reset() { + cancelPendingRequests(); mFeatures.clear(); mLayer = nullptr; mSearchExpression.clear(); @@ -285,6 +311,8 @@ void LayerFeaturesModel::setLayer( QgsVectorLayer *newLayer ) { if ( mLayer != newLayer ) { + cancelPendingRequests(); + if ( mLayer ) { disconnect( mLayer ); diff --git a/app/layerfeaturesmodel.h b/app/layerfeaturesmodel.h index 47995c4ed..cf7823091 100644 --- a/app/layerfeaturesmodel.h +++ b/app/layerfeaturesmodel.h @@ -12,6 +12,7 @@ #include #include +#include #include "qgsvectorlayer.h" #include "featurelayerpair.h" @@ -88,7 +89,7 @@ class LayerFeaturesModel : public FeaturesModel void layerFeaturesCountChanged( int layerFeaturesCount ); //! \a isFetching is TRUE when still fetching results, FALSE when done fetching - bool fetchingResultsChanged( bool isFetching ); + void fetchingResultsChanged( bool isFetching ); protected: @@ -104,26 +105,33 @@ class LayerFeaturesModel : public FeaturesModel void onFutureFinished(); private: + struct SearchResultData + { + int searchId; + QgsFeatureList features; + }; + QString buildSearchExpression(); //! Performs getFeatures on layer. Takes ownership of \a layer and tries to move it to current thread. - QgsFeatureList fetchFeatures( QgsVectorLayerFeatureSource *layer, QgsFeatureRequest req, int searchId ); + static SearchResultData fetchFeatures( QgsVectorLayerFeatureSource *layer, const QgsFeatureRequest &req, int searchId ); //! Returns found attribute and its value from search expression for feature QString searchResultPair( const FeatureLayerPair &feat ) const; + void cancelPendingRequests(); + const int FEATURES_LIMIT = 10000; //!< Number of maximum features loaded from layer QString mSearchExpression; QgsVectorLayer *mLayer = nullptr; QAtomicInt mNextSearchId = 0; - QFutureWatcher mSearchResultWatcher; + QHash mFeedbacks; //!< feedback objects parented to this + QHash*> mSearchResultWatchers; //!< future watcher objects parented to this bool mFetchingResults = false; bool mUseAttributeTableSortOrder = false; - std::unique_ptr mFeedback; - friend class TestModels; }; From 1c64dd2ee59abfe480f39be3b9ebf8ff87f1dbaa Mon Sep 17 00:00:00 2001 From: uclaros Date: Thu, 4 Jun 2026 18:25:54 +0300 Subject: [PATCH 2/5] astyle --- app/layerfeaturesmodel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layerfeaturesmodel.h b/app/layerfeaturesmodel.h index cf7823091..2a872ed78 100644 --- a/app/layerfeaturesmodel.h +++ b/app/layerfeaturesmodel.h @@ -127,7 +127,7 @@ class LayerFeaturesModel : public FeaturesModel QgsVectorLayer *mLayer = nullptr; QAtomicInt mNextSearchId = 0; - QHash mFeedbacks; //!< feedback objects parented to this + QHash mFeedbacks; //!< feedback objects parented to this QHash*> mSearchResultWatchers; //!< future watcher objects parented to this bool mFetchingResults = false; bool mUseAttributeTableSortOrder = false; From aa746d6d6882083259505f446315361cd7700f3c Mon Sep 17 00:00:00 2001 From: uclaros Date: Mon, 8 Jun 2026 11:33:09 +0300 Subject: [PATCH 3/5] Properly clean up pending slow requests on destruction --- app/layerfeaturesmodel.cpp | 102 +++++++++++++++++++++---------------- app/layerfeaturesmodel.h | 13 +++-- 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/app/layerfeaturesmodel.cpp b/app/layerfeaturesmodel.cpp index 0ec29454b..1e0b10367 100644 --- a/app/layerfeaturesmodel.cpp +++ b/app/layerfeaturesmodel.cpp @@ -9,10 +9,13 @@ #include "layerfeaturesmodel.h" -#include "inpututils.h" -#include "qgsproject.h" -#include "qgsvectorlayerfeatureiterator.h" +#include "featurelayerpair.h" + +#include "qgsfeaturerequest.h" #include "qgsfeedback.h" +#include "qgsproject.h" +#include "qgsvectordataprovider.h" +#include "qgsvectorlayer.h" #include #include @@ -27,11 +30,25 @@ LayerFeaturesModel::LayerFeaturesModel( QObject *parent ) LayerFeaturesModel::~LayerFeaturesModel() { - // cancel any long running request - cancelPendingRequests(); + for ( auto [id, feedbackWatcherPair] : mResultWatchers.asKeyValueRange() ) + { + const int i = id; + QgsFeedback *feedback = feedbackWatcherPair.first; + QFutureWatcher *watcher = feedbackWatcherPair.second; + + // watcher should not call ofFutureFinished after this is deleted + watcher->disconnect( this ); + feedback->cancel(); - for ( auto w : std::as_const( mSearchResultWatchers ) ) - w->waitForFinished(); + // Self-cleanup: delete both once the background task finishes + connect( watcher, &QFutureWatcher::finished, watcher, [watcher, feedback, i]() + { + watcher->deleteLater(); + feedback->deleteLater(); + qDebug() << QStringLiteral( "Search (%1) cleaned up in the destructor" ).arg( i ); + } ); + } + mResultWatchers.clear(); } QVariant LayerFeaturesModel::data( const QModelIndex &index, int role ) const @@ -72,10 +89,10 @@ QHash LayerFeaturesModel::roleNames() const void LayerFeaturesModel::populate() { - if ( mLayer ) - { - cancelPendingRequests(); + cancelPendingRequests(); + if ( mLayer && mLayer->dataProvider() ) + { mFetchingResults = true; emit fetchingResultsChanged( mFetchingResults ); beginResetModel(); @@ -86,50 +103,44 @@ void LayerFeaturesModel::populate() setupFeatureRequest( req ); const int searchId = mNextSearchId.fetchAndAddOrdered( 1 ); - QgsFeedback *feedback = new QgsFeedback( this ); - mFeedbacks[ searchId ] = feedback; + QgsFeedback *feedback = new QgsFeedback(); req.setFeedback( feedback ); - QFutureWatcher *watcher = new QFutureWatcher( this ); - mSearchResultWatchers[ searchId ] = watcher; + QFutureWatcher *watcher = new QFutureWatcher(); + + mResultWatchers[ searchId ] = qMakePair( feedback, watcher ); connect( watcher, &QFutureWatcher::finished, this, &LayerFeaturesModel::onFutureFinished ); - QgsVectorLayerFeatureSource *source = new QgsVectorLayerFeatureSource( mLayer ); - watcher->setFuture( QtConcurrent::run( &LayerFeaturesModel::fetchFeatures, source, req, searchId ) ); + qDebug() << QStringLiteral( "Search (%1) starting on layer %2" ).arg( searchId ).arg( mLayer->id() ); + + // We use the data provider source here, so we can have potential sorting on the provider side, while also skipping any uncommitted features + watcher->setFuture( QtConcurrent::run( LayerFeaturesModel::fetchFeatures, mLayer->dataProvider()->featureSource(), req, searchId ) ); } } -LayerFeaturesModel::SearchResultData LayerFeaturesModel::fetchFeatures( QgsVectorLayerFeatureSource *source, const QgsFeatureRequest &req, int searchId ) +LayerFeaturesModel::SearchResultData LayerFeaturesModel::fetchFeatures( QgsAbstractFeatureSource *source, const QgsFeatureRequest &req, int searchId ) { - std::unique_ptr fs( source ); - QgsFeatureList fl; - - // a search might have been queued if no threads were available in the pool, so we also - // check if canceled before we start as the first iteration can potentially be slow - if ( req.feedback()->isCanceled() ) - { - qDebug() << QStringLiteral( "Search (%1) was cancelled before it started!" ).arg( searchId ); - return { searchId, fl }; - } - +#ifdef QT_DEBUG QElapsedTimer t; t.start(); - QgsFeatureIterator it = fs->getFeatures( req ); +#endif + + std::unique_ptr ownedSource( source ); + QgsFeatureList fl; + + QgsFeatureIterator it = ownedSource->getFeatures( req ); + it.setInterruptionChecker( req.feedback() ); QgsFeature f; while ( it.nextFeature( f ) ) { - if ( FID_IS_NEW( f.id() ) || FID_IS_NULL( f.id() ) ) - { - continue; // ignore uncommited features - } - fl.append( f ); } +#ifdef QT_DEBUG const bool canceled = req.feedback()->isCanceled(); - qDebug() << QStringLiteral( "Search (%1) %2 after %3ms, results: %4" ).arg( searchId ).arg( canceled ? QStringLiteral( "was canceled" ) : QStringLiteral( "completed" ) ).arg( t.elapsed() ).arg( fl.count() ); +#endif return { searchId, fl }; } @@ -138,21 +149,20 @@ void LayerFeaturesModel::onFutureFinished() QFutureWatcher *watcher = static_cast< QFutureWatcher *>( sender() ); const SearchResultData data = watcher->future().result(); - mSearchResultWatchers.remove( data.searchId ); - watcher->deleteLater(); - - QgsFeedback *feedback = mFeedbacks.take( data.searchId ); - feedback->deleteLater(); + auto [f, w] = mResultWatchers.take( data.searchId ); + w->deleteLater(); + f->deleteLater(); + qDebug() << QStringLiteral( "Search (%1) cleaned up" ).arg( data.searchId ); // We ignore the results of cancelled requests - if ( !feedback->isCanceled() ) + if ( !f->isCanceled() ) { const QgsFeatureList features = data.features; beginResetModel(); mFeatures.clear(); - for ( const auto &f : features ) + for ( const QgsFeature &feat : features ) { - mFeatures.emplaceBack( f, mLayer ); + mFeatures.emplaceBack( feat, mLayer ); } emit layerFeaturesCountChanged( layerFeaturesCount() ); emit countChanged( rowCount() ); @@ -200,8 +210,10 @@ QString LayerFeaturesModel::searchResultPair( const FeatureLayerPair &pair ) con void LayerFeaturesModel::cancelPendingRequests() { - for ( auto fb : std::as_const( mFeedbacks ) ) - fb->cancel(); + for ( auto [id, feedbackWatcherPair] : mResultWatchers.asKeyValueRange() ) + { + feedbackWatcherPair.first->cancel(); + } } QString LayerFeaturesModel::buildSearchExpression() diff --git a/app/layerfeaturesmodel.h b/app/layerfeaturesmodel.h index 2a872ed78..47a9d44fb 100644 --- a/app/layerfeaturesmodel.h +++ b/app/layerfeaturesmodel.h @@ -14,12 +14,12 @@ #include #include -#include "qgsvectorlayer.h" -#include "featurelayerpair.h" #include "featuresmodel.h" -class QgsVectorLayerFeatureSource; +class QgsVectorLayer; + +class FeatureLayerPair; /** @@ -113,8 +113,8 @@ class LayerFeaturesModel : public FeaturesModel QString buildSearchExpression(); - //! Performs getFeatures on layer. Takes ownership of \a layer and tries to move it to current thread. - static SearchResultData fetchFeatures( QgsVectorLayerFeatureSource *layer, const QgsFeatureRequest &req, int searchId ); + //! Performs getFeatures on a feature source. Takes ownership of \a source. + static SearchResultData fetchFeatures( QgsAbstractFeatureSource *source, const QgsFeatureRequest &req, int searchId ); //! Returns found attribute and its value from search expression for feature QString searchResultPair( const FeatureLayerPair &feat ) const; @@ -127,8 +127,7 @@ class LayerFeaturesModel : public FeaturesModel QgsVectorLayer *mLayer = nullptr; QAtomicInt mNextSearchId = 0; - QHash mFeedbacks; //!< feedback objects parented to this - QHash*> mSearchResultWatchers; //!< future watcher objects parented to this + QHash *>> mResultWatchers; //!< owns feedback and future watches objects bool mFetchingResults = false; bool mUseAttributeTableSortOrder = false; From 1dbadb5bb089a5e1622879a000d1c99590ca68fa Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 10 Jun 2026 14:53:20 +0300 Subject: [PATCH 4/5] Switch back to using QgsVectorLayerFeatureSource instead of featureSource from provider --- app/layerfeaturesmodel.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/layerfeaturesmodel.cpp b/app/layerfeaturesmodel.cpp index 1e0b10367..8f269226b 100644 --- a/app/layerfeaturesmodel.cpp +++ b/app/layerfeaturesmodel.cpp @@ -14,11 +14,11 @@ #include "qgsfeaturerequest.h" #include "qgsfeedback.h" #include "qgsproject.h" -#include "qgsvectordataprovider.h" #include "qgsvectorlayer.h" +#include "qgsvectorlayerfeatureiterator.h" #include -#include +#include #include @@ -113,8 +113,7 @@ void LayerFeaturesModel::populate() qDebug() << QStringLiteral( "Search (%1) starting on layer %2" ).arg( searchId ).arg( mLayer->id() ); - // We use the data provider source here, so we can have potential sorting on the provider side, while also skipping any uncommitted features - watcher->setFuture( QtConcurrent::run( LayerFeaturesModel::fetchFeatures, mLayer->dataProvider()->featureSource(), req, searchId ) ); + watcher->setFuture( QtConcurrent::run( LayerFeaturesModel::fetchFeatures, new QgsVectorLayerFeatureSource( mLayer ), req, searchId ) ); } } @@ -134,6 +133,11 @@ LayerFeaturesModel::SearchResultData LayerFeaturesModel::fetchFeatures( QgsAbstr while ( it.nextFeature( f ) ) { + if ( FID_IS_NEW( f.id() ) || FID_IS_NULL( f.id() ) ) + { + continue; // ignore uncommited features + } + fl.append( f ); } From a0a46c051305d9f7d70f006126d2e894c8a8394a Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 10 Jun 2026 15:28:54 +0300 Subject: [PATCH 5/5] Connect to lambda instead of using sender() --- app/layerfeaturesmodel.cpp | 24 +++++++++++++++--------- app/layerfeaturesmodel.h | 8 ++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/layerfeaturesmodel.cpp b/app/layerfeaturesmodel.cpp index 8f269226b..b7f39cf55 100644 --- a/app/layerfeaturesmodel.cpp +++ b/app/layerfeaturesmodel.cpp @@ -11,9 +11,9 @@ #include "featurelayerpair.h" +#include "qgsattributetableconfig.h" #include "qgsfeaturerequest.h" #include "qgsfeedback.h" -#include "qgsproject.h" #include "qgsvectorlayer.h" #include "qgsvectorlayerfeatureiterator.h" @@ -109,7 +109,7 @@ void LayerFeaturesModel::populate() QFutureWatcher *watcher = new QFutureWatcher(); mResultWatchers[ searchId ] = qMakePair( feedback, watcher ); - connect( watcher, &QFutureWatcher::finished, this, &LayerFeaturesModel::onFutureFinished ); + connect( watcher, &QFutureWatcher::finished, this, [this, searchId] { this->handleFinishedSearch( searchId ); } ); qDebug() << QStringLiteral( "Search (%1) starting on layer %2" ).arg( searchId ).arg( mLayer->id() ); @@ -148,18 +148,24 @@ LayerFeaturesModel::SearchResultData LayerFeaturesModel::fetchFeatures( QgsAbstr return { searchId, fl }; } -void LayerFeaturesModel::onFutureFinished() +void LayerFeaturesModel::handleFinishedSearch( int searchId ) { - QFutureWatcher *watcher = static_cast< QFutureWatcher *>( sender() ); + if ( !mResultWatchers.contains( searchId ) ) + { + // should not happen, this method is called only once for existing searchIds only + Q_ASSERT( false ); + return; + } + + auto [feedback, watcher] = mResultWatchers.take( searchId ); const SearchResultData data = watcher->future().result(); - auto [f, w] = mResultWatchers.take( data.searchId ); - w->deleteLater(); - f->deleteLater(); - qDebug() << QStringLiteral( "Search (%1) cleaned up" ).arg( data.searchId ); + watcher->deleteLater(); + feedback->deleteLater(); + qDebug() << QStringLiteral( "Search (%1) cleaned up" ).arg( searchId ); // We ignore the results of cancelled requests - if ( !f->isCanceled() ) + if ( !feedback->isCanceled() ) { const QgsFeatureList features = data.features; beginResetModel(); diff --git a/app/layerfeaturesmodel.h b/app/layerfeaturesmodel.h index 47a9d44fb..108365ae9 100644 --- a/app/layerfeaturesmodel.h +++ b/app/layerfeaturesmodel.h @@ -101,9 +101,6 @@ class LayerFeaturesModel : public FeaturesModel //! These are the attributes that will be fetched from the data source when populating the model QgsAttributeList mAttributeList; - private slots: - void onFutureFinished(); - private: struct SearchResultData { @@ -116,6 +113,9 @@ class LayerFeaturesModel : public FeaturesModel //! Performs getFeatures on a feature source. Takes ownership of \a source. static SearchResultData fetchFeatures( QgsAbstractFeatureSource *source, const QgsFeatureRequest &req, int searchId ); + //! Should be called when a search is done/canceled. Cleans up mResultWatchers entry and populates the model if not canceled. + void handleFinishedSearch( int searchId ); + //! Returns found attribute and its value from search expression for feature QString searchResultPair( const FeatureLayerPair &feat ) const; @@ -127,7 +127,7 @@ class LayerFeaturesModel : public FeaturesModel QgsVectorLayer *mLayer = nullptr; QAtomicInt mNextSearchId = 0; - QHash *>> mResultWatchers; //!< owns feedback and future watches objects + QHash * >> mResultWatchers; //!< owns feedback and future watches objects bool mFetchingResults = false; bool mUseAttributeTableSortOrder = false;