[forms][feature] Value relation: more options for sorting (#59404)

Implements a few newsorting options for the value relation widget:

- reverse order
- order by a specific field

the default behavior is unchanged (Key).

Fixes #54133

Funded by: Consorzio della Bonifica Renana
This commit is contained in:
Alessandro Pasotti 2024-11-13 02:21:55 +01:00 committed by GitHub
parent e96d090109
commit 3000b7f01f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 309 additions and 138 deletions

View File

@ -31,16 +31,6 @@ using namespace nlohmann;
#include <QSettings>
bool orderByKeyLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 )
{
return p1.group == p2.group ? qgsVariantLessThan( p1.key, p2.key ) : qgsVariantLessThan( p1.group, p2.group );
}
bool orderByValueLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 )
{
return p1.group == p2.group ? qgsVariantLessThan( p1.value, p2.value ) : qgsVariantLessThan( p1.group, p2.group );
}
QgsValueRelationFieldFormatter::QgsValueRelationFieldFormatter()
{
setFlags( flags() | QgsFieldFormatter::CanProvideAvailableValues );
@ -176,6 +166,11 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte
}
QgsFeatureIterator fit = layer->getFeatures( request );
const bool orderByField { config.value( QStringLiteral( "OrderByField" ) ).toBool() };
const int fieldIdx { orderByField ? layer->fields().lookupField( config.value( QStringLiteral( "OrderByFieldName" ) ).toString() ) : -1 };
const bool reverseSort { config.value( QStringLiteral( "OrderByDescending" ) ).toBool() };
QMap<QVariant, QVariant> orderByFieldValues;
QgsFeature f;
while ( fit.nextFeature( f ) )
@ -187,16 +182,47 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte
description = descriptionExpression.evaluate( &context ).toString();
}
const QVariant group = groupIdx > -1 ? f.attribute( groupIdx ) : QVariant();
cache.append( ValueRelationItem( f.attribute( keyIdx ), f.attribute( valueIdx ).toString(), description, group ) );
const QVariant keyValue = f.attribute( keyIdx );
if ( fieldIdx != -1 )
{
orderByFieldValues.insert( keyValue, f.attribute( fieldIdx ) );
}
cache.append( ValueRelationItem( keyValue, f.attribute( valueIdx ).toString(), description, group ) );
}
if ( config.value( QStringLiteral( "OrderByValue" ) ).toBool() )
{
std::sort( cache.begin(), cache.end(), orderByValueLessThan );
std::sort( cache.begin(), cache.end(), [&reverseSort]( const QgsValueRelationFieldFormatter::ValueRelationItem & p1, const QgsValueRelationFieldFormatter::ValueRelationItem & p2 ) -> bool
{
if ( reverseSort )
return p1.group == p2.group ? qgsVariantGreaterThan( p1.value, p2.value ) : qgsVariantGreaterThan( p1.group, p2.group );
else
return p1.group == p2.group ? qgsVariantLessThan( p1.value, p2.value ) : qgsVariantLessThan( p1.group, p2.group );
} );
}
// Order by field
else if ( fieldIdx != -1 )
{
std::sort( cache.begin(), cache.end(), [&reverseSort, &orderByFieldValues]( const QgsValueRelationFieldFormatter::ValueRelationItem & p1, const QgsValueRelationFieldFormatter::ValueRelationItem & p2 ) -> bool
{
if ( reverseSort )
return p1.group == p2.group ? qgsVariantGreaterThan( orderByFieldValues.value( p1.key ), orderByFieldValues.value( p2.key ) ) : qgsVariantGreaterThan( p1.group, p2.group );
else
return p1.group == p2.group ? qgsVariantLessThan( orderByFieldValues.value( p1.key ), orderByFieldValues.value( p2.key ) ) : qgsVariantLessThan( p1.group, p2.group );
} );
}
// OrderByKey is the default
else
{
std::sort( cache.begin(), cache.end(), orderByKeyLessThan );
std::sort( cache.begin(), cache.end(), [&reverseSort]( const QgsValueRelationFieldFormatter::ValueRelationItem & p1, const QgsValueRelationFieldFormatter::ValueRelationItem & p2 ) -> bool
{
if ( reverseSort )
return p1.group == p2.group ? qgsVariantGreaterThan( p1.key, p2.key ) : qgsVariantGreaterThan( p1.group, p2.group );
else
return p1.group == p2.group ? qgsVariantLessThan( p1.key, p2.key ) : qgsVariantLessThan( p1.group, p2.group );
} );
}
return cache;

View File

@ -31,12 +31,20 @@ QgsValueRelationConfigDlg::QgsValueRelationConfigDlg( QgsVectorLayer *vl, int fi
mGroupColumn->setLayer( mLayerName->currentLayer() );
mGroupColumn->setAllowEmptyFieldName( true );
mDescriptionExpression->setLayer( mLayerName->currentLayer() );
mOrderByFieldName->setLayer( mLayerName->currentLayer() );
mOrderByFieldName->setAllowEmptyFieldName( false );
connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mKeyColumn, &QgsFieldComboBox::setLayer );
connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mValueColumn, &QgsFieldComboBox::setLayer );
connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mGroupColumn, &QgsFieldComboBox::setLayer );
connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mDescriptionExpression, &QgsFieldExpressionWidget::setLayer );
connect( mLayerName, &QgsMapLayerComboBox::layerChanged, this, &QgsValueRelationConfigDlg::layerChanged );
connect( mEditExpression, &QAbstractButton::clicked, this, &QgsValueRelationConfigDlg::editExpression );
connect( mOrderByField, &QAbstractButton::toggled, mOrderByFieldName, [ = ]( bool enabled )
{
mOrderByFieldName->setEnabled( enabled );
} );
mOrderByGroupBox->setCollapsed( true );
mNofColumns->setMinimum( 1 );
mNofColumns->setMaximum( 10 );
@ -93,6 +101,10 @@ QVariantMap QgsValueRelationConfigDlg::config()
cfg.insert( QStringLiteral( "NofColumns" ), mNofColumns->value() );
cfg.insert( QStringLiteral( "AllowNull" ), mAllowNull->isChecked() );
cfg.insert( QStringLiteral( "OrderByValue" ), mOrderByValue->isChecked() );
cfg.insert( QStringLiteral( "OrderByKey" ), mOrderByKey->isChecked() );
cfg.insert( QStringLiteral( "OrderByField" ), mOrderByField->isChecked() );
cfg.insert( QStringLiteral( "OrderByFieldName" ), mOrderByFieldName->currentField() );
cfg.insert( QStringLiteral( "OrderByDescending" ), mOrderByDescending->isChecked() );
cfg.insert( QStringLiteral( "FilterExpression" ), mFilterExpression->toPlainText() );
cfg.insert( QStringLiteral( "UseCompleter" ), mUseCompleter->isChecked() );
const Qt::MatchFlags completerMatchFlags { mCompleterMatchFromStart->isChecked() ? Qt::MatchFlag::MatchStartsWith : Qt::MatchFlag::MatchContains };
@ -105,6 +117,7 @@ void QgsValueRelationConfigDlg::setConfig( const QVariantMap &config )
{
QgsVectorLayer *lyr = QgsValueRelationFieldFormatter::resolveLayer( config, QgsProject::instance() );
mLayerName->setLayer( lyr );
mOrderByFieldName->setLayer( lyr );
mKeyColumn->setField( config.value( QStringLiteral( "Key" ) ).toString() );
mValueColumn->setField( config.value( QStringLiteral( "Value" ) ).toString() );
mGroupColumn->setField( config.value( QStringLiteral( "Group" ) ).toString() );
@ -119,6 +132,22 @@ void QgsValueRelationConfigDlg::setConfig( const QVariantMap &config )
}
mAllowNull->setChecked( config.value( QStringLiteral( "AllowNull" ) ).toBool() );
mOrderByValue->setChecked( config.value( QStringLiteral( "OrderByValue" ) ).toBool() );
mOrderByField->setChecked( config.value( QStringLiteral( "OrderByField" ) ).toBool() );
mOrderByKey->setChecked( config.value( QStringLiteral( "OrderByKey" ) ).toBool() );
mOrderByFieldName->setField( config.value( QStringLiteral( "OrderByFieldName" ) ).toString() );
mOrderByDescending->setChecked( config.value( QStringLiteral( "OrderByDescending" ) ).toBool() );
if ( !mOrderByField->isChecked() && !mOrderByValue->isChecked() && !mOrderByKey->isChecked() )
{
mOrderByKey->setChecked( true );
}
// order by key is the default, if it is not checked, expand the config
if ( !mOrderByKey->isChecked() )
{
mOrderByGroupBox->setCollapsed( false );
}
mFilterExpression->setPlainText( config.value( QStringLiteral( "FilterExpression" ) ).toString() );
mUseCompleter->setChecked( config.value( QStringLiteral( "UseCompleter" ) ).toBool() );
// Default is MatchStartsWith for backwards compatibility
@ -130,6 +159,7 @@ void QgsValueRelationConfigDlg::layerChanged()
{
mFilterExpression->setEnabled( qobject_cast<QgsVectorLayer *>( mLayerName->currentLayer() ) );
mEditExpression->setEnabled( qobject_cast<QgsVectorLayer *>( mLayerName->currentLayer() ) );
mOrderByFieldName->setLayer( mLayerName->currentLayer() );
}
void QgsValueRelationConfigDlg::editExpression()

View File

@ -6,15 +6,116 @@
<rect>
<x>0</x>
<y>0</y>
<width>318</width>
<height>490</height>
<width>342</width>
<height>620</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0">
<item row="14" column="0" colspan="2">
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Key column</string>
</property>
<property name="buddy">
<cstring>mKeyColumn</cstring>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QCheckBox" name="mUseCompleter">
<property name="text">
<string>Use completer</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="QgsSpinBox" name="mNofColumns">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Group column</string>
</property>
<property name="buddy">
<cstring>mGroupColumn</cstring>
</property>
</widget>
</item>
<item row="20" column="0" colspan="2">
<widget class="QTextEdit" name="mFilterExpression"/>
</item>
<item row="1" column="1">
<widget class="QgsMapLayerComboBox" name="mLayerName"/>
</item>
<item row="2" column="1">
<widget class="QgsFieldComboBox" name="mKeyColumn"/>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Select layer, key column and value column</string>
</property>
</widget>
</item>
<item row="15" column="0" colspan="2">
<widget class="QCheckBox" name="mAllowMulti">
<property name="text">
<string>Allow multiple selections</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Layer</string>
</property>
<property name="buddy">
<cstring>mLayerName</cstring>
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<widget class="QCheckBox" name="mAllowNull">
<property name="text">
<string>Allow NULL value</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Value column</string>
</property>
<property name="buddy">
<cstring>mValueColumn</cstring>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QCheckBox" name="mCompleterMatchFromStart">
<property name="text">
<string>Only match from the beginning of the string</string>
</property>
</widget>
</item>
<item row="17" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_19">
@ -32,7 +133,7 @@
<enum>Qt::RightToLeft</enum>
</property>
<property name="icon">
<iconset>
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/mIconExpression.svg</normaloff>:/images/themes/default/mIconExpression.svg</iconset>
</property>
</widget>
@ -52,137 +153,85 @@
</item>
</layout>
</item>
<item row="13" column="0">
<widget class="QLabel" name="label_nofColumns">
<property name="text">
<string>Number of columns</string>
</property>
</widget>
</item>
<item row="17" column="0" colspan="2">
<widget class="QTextEdit" name="mFilterExpression"/>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Select layer, key column and value column</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Layer</string>
</property>
<property name="buddy">
<cstring>mLayerName</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QgsFieldComboBox" name="mValueColumn"/>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="mOrderByValue">
<property name="text">
<string>Order by value</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QgsMapLayerComboBox" name="mLayerName"/>
</item>
<item row="12" column="0" colspan="2">
<widget class="QCheckBox" name="mAllowMulti">
<property name="text">
<string>Allow multiple selections</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="mAllowNull">
<property name="text">
<string>Allow NULL value</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QCheckBox" name="mUseCompleter">
<property name="text">
<string>Use completer</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QCheckBox" name="mCompleterMatchFromStart">
<property name="text">
<string>Only match from the beginning of the string</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Key column</string>
</property>
<property name="buddy">
<cstring>mKeyColumn</cstring>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QgsSpinBox" name="mNofColumns">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QgsFieldComboBox" name="mKeyColumn"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Value column</string>
</property>
<property name="buddy">
<cstring>mValueColumn</cstring>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Group column</string>
</property>
<property name="buddy">
<cstring>mGroupColumn</cstring>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="8" column="1">
<widget class="QgsFieldComboBox" name="mGroupColumn"/>
</item>
<item row="6" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="mDisplayGroupName">
<property name="text">
<string>Display group name</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label">
<item row="3" column="1">
<widget class="QgsFieldComboBox" name="mValueColumn"/>
</item>
<item row="16" column="0">
<widget class="QLabel" name="label_nofColumns">
<property name="text">
<string>Description</string>
<string>Number of columns</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="10" column="1">
<widget class="QgsFieldExpressionWidget" name="mDescriptionExpression" native="true"/>
</item>
<item row="4" column="0" rowspan="2" colspan="2">
<widget class="QgsCollapsibleGroupBox" name="mOrderByGroupBox">
<property name="title">
<string>Order By</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="mOrderByKey">
<property name="toolTip">
<string>Use the values in the key column for sorting</string>
</property>
<property name="text">
<string>Key</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="mOrderByValue">
<property name="toolTip">
<string>Use the values in the value column for sorting</string>
</property>
<property name="text">
<string>Value</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="mOrderByField">
<property name="toolTip">
<string>Use the values from a specific field for sorting</string>
</property>
<property name="text">
<string>Field</string>
</property>
</widget>
</item>
<item>
<widget class="QgsFieldComboBox" name="mOrderByFieldName"/>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="mOrderByDescending">
<property name="toolTip">
<string>Reverse the sorting</string>
</property>
<property name="text">
<string>Descending order</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
@ -207,6 +256,12 @@
<extends>QComboBox</extends>
<header>qgsmaplayercombobox.h</header>
</customwidget>
<customwidget>
<class>QgsCollapsibleGroupBox</class>
<extends>QGroupBox</extends>
<header>qgscollapsiblegroupbox.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>mLayerName</tabstop>
@ -214,7 +269,6 @@
<tabstop>mValueColumn</tabstop>
<tabstop>mGroupColumn</tabstop>
<tabstop>mDescriptionExpression</tabstop>
<tabstop>mOrderByValue</tabstop>
<tabstop>mAllowNull</tabstop>
<tabstop>mAllowMulti</tabstop>
<tabstop>mUseCompleter</tabstop>
@ -223,6 +277,8 @@
<tabstop>mEditExpression</tabstop>
<tabstop>mFilterExpression</tabstop>
</tabstops>
<resources/>
<resources>
<include location="../../../images/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -39,6 +39,8 @@ class TestQgsValueRelationFieldFormatter: public QObject
void testDependencies();
void testSortValueNull();
void testGroup();
void testOrderBy_data();
void testOrderBy();
private:
std::unique_ptr<QgsVectorLayer> mLayer1;
@ -112,7 +114,7 @@ void TestQgsValueRelationFieldFormatter::init()
QgsFeature ft3( mLayer2->fields() );
ft3.setAttribute( QStringLiteral( "pk" ), 11 );
ft3.setAttribute( QStringLiteral( "material" ), "iron" );
ft3.setAttribute( QStringLiteral( "diameter" ), 120 );
ft3.setAttribute( QStringLiteral( "diameter" ), 110 );
ft3.setAttribute( QStringLiteral( "raccord" ), "sleeve" );
mLayer2->startEditing();
mLayer2->addFeature( ft3 );
@ -121,7 +123,7 @@ void TestQgsValueRelationFieldFormatter::init()
QgsFeature ft4( mLayer2->fields() );
ft4.setAttribute( QStringLiteral( "pk" ), 12 );
ft4.setAttribute( QStringLiteral( "material" ), "steel" );
ft4.setAttribute( QStringLiteral( "diameter" ), 120 );
ft4.setAttribute( QStringLiteral( "diameter" ), 100 );
ft4.setAttribute( QStringLiteral( "raccord" ), "collar" );
mLayer2->startEditing();
mLayer2->addFeature( ft4 );
@ -180,5 +182,62 @@ void TestQgsValueRelationFieldFormatter::testGroup()
QCOMPARE( cache.at( cache.size() - 1 ).group, QVariant( QStringLiteral( "steel" ) ) );
}
void TestQgsValueRelationFieldFormatter::testOrderBy_data()
{
QTest::addColumn<QString>( "orderBy" );
QTest::addColumn<QString>( "fieldName" );
QTest::addColumn<QString>( "expectedFirst" );
QTest::addColumn<QString>( "expectedLast" );
QTest::newRow( "orderByDefault(pk)" ) << QString() << QString() << "brides" << "collar";
QTest::newRow( "orderByKey(pk)" ) << QString() << QStringLiteral( "Key" ) << "brides" << "collar";
QTest::newRow( "orderByValue(raccord)" ) << QString() << QStringLiteral( "Value" ) << "brides" << "collar";
QTest::newRow( "orderByField(raccord)" ) << QStringLiteral( "Field" ) << QStringLiteral( "raccord" ) << "brides" << "sleeve";
QTest::newRow( "orderByField(diameter)" ) << QStringLiteral( "Field" ) << QStringLiteral( "diameter" ) << "collar" << "brides";
QTest::newRow( "orderByField(material)" ) << QStringLiteral( "Field" ) << QStringLiteral( "material" ) << "brides" << "collar";
}
void TestQgsValueRelationFieldFormatter::testOrderBy()
{
QFETCH( QString, orderBy );
QFETCH( QString, fieldName );
QFETCH( QString, expectedFirst );
QFETCH( QString, expectedLast );
QVariantMap config;
config.insert( QStringLiteral( "Layer" ), mLayer2->id() );
config.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) );
config.insert( QStringLiteral( "Value" ), QStringLiteral( "raccord" ) );
if ( !orderBy.isEmpty() )
{
config.insert( QStringLiteral( "OrderBy%1" ).arg( orderBy ), true );
}
if ( !fieldName.isEmpty() )
{
config.insert( QStringLiteral( "OrderByFieldName" ), fieldName );
}
// Ascending
{
const QgsValueRelationFieldFormatter formatter;
QgsValueRelationFieldFormatter::ValueRelationCache cache = formatter.createCache( config );
QVERIFY( !cache.isEmpty() );
QCOMPARE( cache.at( 0 ).value, expectedFirst );
QCOMPARE( cache.at( mLayer2->featureCount() - 1 ).value, expectedLast );
}
// Descending
{
config.insert( QStringLiteral( "OrderByDescending" ), true );
const QgsValueRelationFieldFormatter formatter;
QgsValueRelationFieldFormatter::ValueRelationCache cache = formatter.createCache( config );
QVERIFY( !cache.isEmpty() );
QCOMPARE( cache.at( 0 ).value, expectedLast );
QCOMPARE( cache.at( mLayer2->featureCount() - 1 ).value, expectedFirst );
}
}
QGSTEST_MAIN( TestQgsValueRelationFieldFormatter )
#include "testqgsvaluerelationfieldformatter.moc"