Skip to content

Commit

Permalink
Merge pull request #51282 from troopa81/lazy_load_widget_attribute_table
Browse files Browse the repository at this point in the history
[AttributeTable] Lazy load widget information for attributes
  • Loading branch information
elpaso committed Jan 15, 2023
2 parents bed001f + 568ae8a commit 803fb9f
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 84 deletions.
85 changes: 39 additions & 46 deletions src/gui/attributetable/qgsattributetablemodel.cpp
Expand Up @@ -251,10 +251,8 @@ void QgsAttributeTableModel::featureAdded( QgsFeatureId fid )
{
if ( cache.sortFieldIndex >= 0 )
{
QgsFieldFormatter *fieldFormatter = mFieldFormatters.at( cache.sortFieldIndex );
const QVariant &widgetCache = mAttributeWidgetCaches.at( cache.sortFieldIndex );
const QVariantMap &widgetConfig = mWidgetConfigs.at( cache.sortFieldIndex );
const QVariant sortValue = fieldFormatter->sortValue( mLayer, cache.sortFieldIndex, widgetConfig, widgetCache, mFeat.attribute( cache.sortFieldIndex ) );
const WidgetData &widgetData = getWidgetData( cache.sortFieldIndex );
const QVariant sortValue = widgetData.fieldFormatter->sortValue( mLayer, cache.sortFieldIndex, widgetData.config, widgetData.cache, mFeat.attribute( cache.sortFieldIndex ) );
cache.sortCache.insert( mFeat.id(), sortValue );
}
else if ( cache.sortCacheExpression.isValid() )
Expand Down Expand Up @@ -315,19 +313,16 @@ void QgsAttributeTableModel::layerDeleted()
mLayer = nullptr;
removeRows( 0, rowCount() );

mAttributeWidgetCaches.clear();
mAttributes.clear();
mWidgetFactories.clear();
mWidgetConfigs.clear();
mFieldFormatters.clear();
mWidgetDatas.clear();
}

void QgsAttributeTableModel::fieldFormatterRemoved( QgsFieldFormatter *fieldFormatter )
{
for ( int i = 0; i < mFieldFormatters.size(); ++i )
for ( WidgetData &widgetData : mWidgetDatas )
{
if ( mFieldFormatters.at( i ) == fieldFormatter )
mFieldFormatters[i] = QgsApplication::fieldFormatterRegistry()->fallbackFieldFormatter();
if ( widgetData.fieldFormatter == fieldFormatter )
widgetData.fieldFormatter = QgsApplication::fieldFormatterRegistry()->fallbackFieldFormatter();
}
}

Expand Down Expand Up @@ -355,10 +350,8 @@ void QgsAttributeTableModel::attributeValueChanged( QgsFeatureId fid, int idx, c
}
else
{
QgsFieldFormatter *fieldFormatter = mFieldFormatters.at( cache.sortFieldIndex );
const QVariant &widgetCache = mAttributeWidgetCaches.at( cache.sortFieldIndex );
const QVariantMap &widgetConfig = mWidgetConfigs.at( cache.sortFieldIndex );
const QVariant sortValue = fieldFormatter->representValue( mLayer, cache.sortFieldIndex, widgetConfig, widgetCache, value );
const WidgetData &widgetData = getWidgetData( cache.sortFieldIndex );
const QVariant sortValue = widgetData.fieldFormatter->representValue( mLayer, cache.sortFieldIndex, widgetData.config, widgetData.cache, value );
cache.sortCache.insert( fid, sortValue );
}
}
Expand Down Expand Up @@ -422,22 +415,10 @@ void QgsAttributeTableModel::loadAttributes()

QgsAttributeList attributes;

mWidgetFactories.clear();
mAttributeWidgetCaches.clear();
mWidgetConfigs.clear();
mFieldFormatters.clear();
mWidgetDatas.clear();

for ( int idx = 0; idx < fields.count(); ++idx )
{
const QgsEditorWidgetSetup setup = QgsGui::editorWidgetRegistry()->findBest( mLayer, fields[idx].name() );
QgsEditorWidgetFactory *widgetFactory = QgsGui::editorWidgetRegistry()->factory( setup.type() );
QgsFieldFormatter *fieldFormatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() );

mWidgetFactories.append( widgetFactory );
mWidgetConfigs.append( setup.config() );
mAttributeWidgetCaches.append( fieldFormatter->createCache( mLayer, idx, setup.config() ) );
mFieldFormatters.append( fieldFormatter );

attributes << idx;
}

Expand All @@ -454,6 +435,7 @@ void QgsAttributeTableModel::loadAttributes()

mFieldCount = attributes.size();
mAttributes = attributes;
mWidgetDatas.resize( mFieldCount );

for ( SortCache &cache : mSortCaches )
{
Expand Down Expand Up @@ -716,7 +698,8 @@ QVariant QgsAttributeTableModel::data( const QModelIndex &index, int role ) cons

if ( role == Qt::TextAlignmentRole )
{
return static_cast<Qt::Alignment::Int>( mFieldFormatters.at( index.column() )->alignmentFlag( mLayer, fieldId, mWidgetConfigs.at( index.column() ) ) | Qt::AlignVCenter );
const WidgetData &widgetData = getWidgetData( index.column() );
return static_cast<Qt::Alignment::Int>( widgetData.fieldFormatter->alignmentFlag( mLayer, fieldId, widgetData.config ) | Qt::AlignVCenter );
}

if ( mFeat.id() != rowId || !mFeat.isValid() )
Expand All @@ -733,18 +716,14 @@ QVariant QgsAttributeTableModel::data( const QModelIndex &index, int role ) cons
switch ( role )
{
case Qt::DisplayRole:
return mFieldFormatters.at( index.column() )->representValue( mLayer,
fieldId,
mWidgetConfigs.at( index.column() ),
mAttributeWidgetCaches.at( index.column() ),
val );
{
const WidgetData &widgetData = getWidgetData( index.column() );
return widgetData.fieldFormatter->representValue( mLayer, fieldId, widgetData.config, widgetData.cache, val );
}
case Qt::ToolTipRole:
{
QString tooltip = mFieldFormatters.at( index.column() )->representValue( mLayer,
fieldId,
mWidgetConfigs.at( index.column() ),
mAttributeWidgetCaches.at( index.column() ),
val );
const WidgetData &widgetData = getWidgetData( index.column() );
QString tooltip = widgetData.fieldFormatter->representValue( mLayer, fieldId, widgetData.config, widgetData.cache, val );
if ( val.type() == QVariant::String && QgsStringUtils::isUrl( val.toString() ) )
{
tooltip = tr( "%1 (Ctrl+click to open)" ).arg( tooltip );
Expand Down Expand Up @@ -976,9 +955,7 @@ void QgsAttributeTableModel::prefetchSortData( const QString &expressionString,
return;
}

QgsFieldFormatter *fieldFormatter = nullptr;
QVariant widgetCache;
QVariantMap widgetConfig;
WidgetData widgetData;

if ( cache.sortCacheExpression.isField() )
{
Expand All @@ -1001,9 +978,7 @@ void QgsAttributeTableModel::prefetchSortData( const QString &expressionString,
{
cache.sortCacheAttributes.append( cache.sortFieldIndex );

widgetCache = mAttributeWidgetCaches.at( cache.sortFieldIndex );
widgetConfig = mWidgetConfigs.at( cache.sortFieldIndex );
fieldFormatter = mFieldFormatters.at( cache.sortFieldIndex );
widgetData = getWidgetData( cache.sortFieldIndex );
}

const QgsFeatureRequest request = QgsFeatureRequest( mFeatureRequest )
Expand All @@ -1022,7 +997,7 @@ void QgsAttributeTableModel::prefetchSortData( const QString &expressionString,
}
else
{
const QVariant sortValue = fieldFormatter->sortValue( mLayer, cache.sortFieldIndex, widgetConfig, widgetCache, f.attribute( cache.sortFieldIndex ) );
const QVariant sortValue = widgetData.fieldFormatter->sortValue( mLayer, cache.sortFieldIndex, widgetData.config, widgetData.cache, f.attribute( cache.sortFieldIndex ) );
cache.sortCache.insert( f.id(), sortValue );
}
}
Expand Down Expand Up @@ -1056,3 +1031,21 @@ const QgsFeatureRequest &QgsAttributeTableModel::request() const
{
return mFeatureRequest;
}

const QgsAttributeTableModel::WidgetData &QgsAttributeTableModel::getWidgetData( int column ) const
{
Q_ASSERT( column >= 0 && column < mAttributes.size() );

WidgetData &widgetData = mWidgetDatas[ column ];
if ( !widgetData.loaded )
{
const int idx = fieldIdx( column );
const QgsEditorWidgetSetup setup = QgsGui::editorWidgetRegistry()->findBest( mLayer, mFields[ idx ].name() );
widgetData.fieldFormatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() );
widgetData.config = setup.config();
widgetData.cache = widgetData.fieldFormatter->createCache( mLayer, idx, setup.config() );
widgetData.loaded = true;
}

return widgetData;
}
20 changes: 15 additions & 5 deletions src/gui/attributetable/qgsattributetablemodel.h
Expand Up @@ -337,21 +337,31 @@ class GUI_EXPORT QgsAttributeTableModel: public QAbstractTableModel

QgsFields mFields;
QgsAttributeList mAttributes;
QVector<QgsEditorWidgetFactory *> mWidgetFactories;
QVector<QgsFieldFormatter *> mFieldFormatters;
QVector<QVariant> mAttributeWidgetCaches;
QVector<QVariantMap> mWidgetConfigs;

struct WidgetData
{
QgsFieldFormatter *fieldFormatter = nullptr;
QVariant cache;
QVariantMap config;
bool loaded = false;
};
mutable QVector<WidgetData> mWidgetDatas;

QHash<QgsFeatureId, int> mIdRowMap;
QHash<int, QgsFeatureId> mRowIdMap;
mutable QHash<QgsFeatureId, QList<QgsConditionalStyle> > mRowStylesMap;

mutable QgsExpressionContext mExpressionContext;

/**
* Returns widget information for \a column index
*/
const WidgetData &getWidgetData( int column ) const;

/**
* Gets mFieldCount, mAttributes
*/
virtual void loadAttributes();
void loadAttributes();

/**
* Load feature fid into local cache (mFeat)
Expand Down
87 changes: 54 additions & 33 deletions tests/src/python/test_qgsattributetablemodel.py
Expand Up @@ -51,9 +51,30 @@ class TestQgsAttributeTableModel(unittest.TestCase):
def setUpClass(cls):
QgsGui.editorWidgetRegistry().initEditors()

# to track down whether or not we have created widget regarding the field
class TestEditorWidgetFactory(QgsEditorWidgetFactory):

def __init__(self):
super().__init__("test")
self.widgetLoaded = 0

def create(self, vl, fieldIdx, editor, parent):
return None

def configWidget(self, vl, fieldIdx, parent):
return None

def fieldScore(self, vl, fieldIdx):
self.widgetLoaded += 1
return 0

cls.testWidgetFactory = TestEditorWidgetFactory()
QgsGui.editorWidgetRegistry().registerWidget("testWidget", cls.testWidgetFactory)

def setUp(self):
self.layer = self.createLayer()
self.cache = QgsVectorLayerCache(self.layer, 100)
self.testWidgetFactory.widgetLoaded = 0
self.am = QgsAttributeTableModel(self.cache)
self.am.loadLayer()

Expand Down Expand Up @@ -282,32 +303,14 @@ def testTransactionRollback(self):
self.assertEqual(len([f for f in vl.getFeatures()]), 1)
self.assertEqual(am.rowCount(), 1)

def testExtraColumns(self):
def testColumnLazyLoading(self):
"""
Test that models handles correctly extra columns
and that attribute loading is done only when needed
Test that widget are loaded only when column is needed
and that models handles correctly extra columns
"""

# to track down whether or not we have created widget regarding the field
widgetLoaded = 0

class TestEditorWidgetFactory(QgsEditorWidgetFactory):

def __init__(self):
super().__init__("test")

def create(self, vl, fieldIdx, editor, parent):
return None

def configWidget(self, vl, fieldIdx, parent):
return None

def fieldScore(self, vl, fieldIdx):
nonlocal widgetLoaded
widgetLoaded += 1
return 0

QgsGui.editorWidgetRegistry().registerWidget("testWidget", TestEditorWidgetFactory())
twf = self.testWidgetFactory
self.assertEqual(twf.widgetLoaded, 0)

# to track down if column have been inserted or removed
colsInserted = 0
Expand All @@ -331,7 +334,7 @@ def __init__(self):
super().__init__()

def data(self, index, role):
if role == Qt.DisplayRole and index.column() > 1:
if role == Qt.DisplayRole and self.sourceModel().extraColumns() > 0 and index.column() > 1:
return f"extra_{index.column()}"

return super().data(index, role)
Expand All @@ -343,9 +346,13 @@ def data(self, index, role):
self.assertEqual(fm.data(fm.index(2, 1), Qt.DisplayRole), "2")
self.assertEqual(fm.data(fm.index(2, 2), Qt.DisplayRole), None)

# 2 columns have been loaded since we call data
self.assertEqual(twf.widgetLoaded, 2)
twf.widgetLoaded = 0

# only one column inserted, no widget loaded
self.am.setExtraColumns(1)
self.assertEqual(widgetLoaded, 0)
self.assertEqual(twf.widgetLoaded, 0)
self.assertEqual(colsInserted, 1)
colsInserted = 0
self.assertEqual(colsRemoved, 0)
Expand All @@ -354,9 +361,11 @@ def data(self, index, role):
self.assertEqual(fm.data(fm.index(2, 1), Qt.DisplayRole), "2")
self.assertEqual(fm.data(fm.index(2, 2), Qt.DisplayRole), "extra_2")

self.assertEqual(twf.widgetLoaded, 0)

# only one column removed, no widget loaded
self.am.setExtraColumns(0)
self.assertEqual(widgetLoaded, 0)
self.assertEqual(twf.widgetLoaded, 0)
self.assertEqual(colsInserted, 0)
self.assertEqual(colsRemoved, 1)
colsRemoved = 0
Expand All @@ -365,27 +374,39 @@ def data(self, index, role):
self.assertEqual(fm.data(fm.index(2, 1), Qt.DisplayRole), "2")
self.assertEqual(fm.data(fm.index(2, 2), Qt.DisplayRole), None)

self.assertEqual(twf.widgetLoaded, 0)

# nothing has changed, nothing should happened
self.am.loadLayer()
self.assertEqual(widgetLoaded, 0)
self.assertEqual(twf.widgetLoaded, 0)
self.assertEqual(colsInserted, 0)
self.assertEqual(colsRemoved, 0)

# add field, widget are reloaded
# add field, widget will be reloaded when data will be called
self.layer.addExpressionField("'newfield_' || \"fldtxt\"", QgsField("newfield", QVariant.String))
self.assertEqual(widgetLoaded, 3)
self.assertEqual(twf.widgetLoaded, 0)
self.assertEqual(colsInserted, 1)
self.assertEqual(colsRemoved, 0)
colsInserted = 0
widgetLoaded = 0
twf.widgetLoaded = 0

self.assertEqual(fm.data(fm.index(2, 0), Qt.DisplayRole), "test")
self.assertEqual(fm.data(fm.index(2, 1), Qt.DisplayRole), "2")
self.assertEqual(fm.data(fm.index(2, 2), Qt.DisplayRole), "newfield_test")
twf.widgetLoaded = 0

# remove field, widget are loaded again
# remove field, widget will be reloaded again
self.layer.removeExpressionField(2)
self.assertEqual(widgetLoaded, 2)
self.assertEqual(twf.widgetLoaded, 0)
self.assertEqual(colsInserted, 0)
self.assertEqual(colsRemoved, 1)
colsRemoved = 0
widgetLoaded = 0

self.assertEqual(fm.data(fm.index(2, 0), Qt.DisplayRole), "test")
self.assertEqual(fm.data(fm.index(2, 1), Qt.DisplayRole), "2")
self.assertEqual(fm.data(fm.index(2, 2), Qt.DisplayRole), None)
self.assertEqual(twf.widgetLoaded, 2)
twf.widgetLoaded = 0


if __name__ == '__main__':
Expand Down

0 comments on commit 803fb9f

Please sign in to comment.