From 700dbe7eeda53170cabfcc6b478c6ee6ff52241d Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 24 Jun 2024 18:42:29 +0200 Subject: [PATCH] [FEATURE] [Identify tool] Present referencing relations (in addition to referenced ones), and support them on arbitrary nesting level Fixes #48776 Previously only referenced relations where presented when exploring a feature, now referenced ones are presented as well. Do that only when the user explicit unfolds a node, to avoid potential 'explosion' of the number of features in the tree in case of a huge database where all features would be connected through relations. If through relations a feature already present in an ancestor node is detected, it is omitted from the related features. Add setting parameters to disable showing referenced and referencing relations. Add a contextual menu item "Explore Feature" to be able to "re-center" the result of the identification tree on a nested feature, to limit the nesting depth. --- src/app/qgsidentifyresultsdialog.cpp | 187 ++++++++++-- src/app/qgsidentifyresultsdialog.h | 46 ++- src/ui/qgsidentifyresultsbase.ui | 22 ++ tests/src/app/CMakeLists.txt | 1 + .../src/app/testqgsidentifyresultsdialog.cpp | 270 ++++++++++++++++++ 5 files changed, 499 insertions(+), 27 deletions(-) create mode 100644 tests/src/app/testqgsidentifyresultsdialog.cpp diff --git a/src/app/qgsidentifyresultsdialog.cpp b/src/app/qgsidentifyresultsdialog.cpp index 3e4fc09ee144..4aa63d13bc0c 100644 --- a/src/app/qgsidentifyresultsdialog.cpp +++ b/src/app/qgsidentifyresultsdialog.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +108,10 @@ constexpr int REPRESENTED_VALUE_ROLE = Qt::UserRole + 2; const QgsSettingsEntryBool *QgsIdentifyResultsDialog::settingHideNullValues = new QgsSettingsEntryBool( QStringLiteral( "hide-null-values" ), QgsSettingsTree::sTreeMap, false, QStringLiteral( "Whether to hide attributes with NULL values in the identify feature result" ) ); +const QgsSettingsEntryBool *QgsIdentifyResultsDialog::settingShowReferencingRelations = new QgsSettingsEntryBool( QStringLiteral( "show-referencing-relations" ), QgsSettingsTree::sTreeMap, true, QStringLiteral( "Whether to show referencing relations in the identify feature result" ) ); + +const QgsSettingsEntryBool *QgsIdentifyResultsDialog::settingShowReferencedRelations = new QgsSettingsEntryBool( QStringLiteral( "show-referenced-relations" ), QgsSettingsTree::sTreeMap, true, QStringLiteral( "Whether to show referenced relations in the identify feature result" ) ); + QgsIdentifyResultsWebView::QgsIdentifyResultsWebView( QWidget *parent ) : QgsWebView( parent ) { @@ -295,6 +300,14 @@ QgsIdentifyResultsFeatureItem::QgsIdentifyResultsFeatureItem( const QgsFields &f { } +QgsIdentifyResultsRelationItem::QgsIdentifyResultsRelationItem( const QStringList &strings, const QgsRelation &relation, bool isReferencedRole, const QgsFeature &topFeature ) + : QTreeWidgetItem( strings ) + , mRelation( relation ) + , mIsReferencedRole( isReferencedRole ) + , mTopFeature( topFeature ) +{ +} + void QgsIdentifyResultsWebViewItem::setHtml( const QString &html ) { mWebView->setHtml( html ); @@ -361,6 +374,8 @@ QgsIdentifyResultsDialog::QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidge connect( mActionAutoFeatureForm, &QAction::toggled, this, &QgsIdentifyResultsDialog::mActionAutoFeatureForm_toggled ); connect( mActionHideDerivedAttributes, &QAction::toggled, this, &QgsIdentifyResultsDialog::mActionHideDerivedAttributes_toggled ); connect( mActionHideNullValues, &QAction::toggled, this, &QgsIdentifyResultsDialog::mActionHideNullValues_toggled ); + connect( mActionShowReferencingRelations, &QAction::toggled, this, &QgsIdentifyResultsDialog::mActionShowReferencingRelations_toggled ); + connect( mActionShowReferencedRelations, &QAction::toggled, this, &QgsIdentifyResultsDialog::mActionShowReferencedRelations_toggled ); mOpenFormAction->setDisabled( true ); @@ -442,6 +457,9 @@ QgsIdentifyResultsDialog::QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidge connect( lstResults, &QTreeWidget::itemClicked, this, &QgsIdentifyResultsDialog::itemClicked ); + connect( lstResults, &QTreeWidget::itemExpanded, + this, &QgsIdentifyResultsDialog::itemExpanded ); + #if defined( HAVE_QTPRINTER ) connect( mActionPrint, &QAction::triggered, this, &QgsIdentifyResultsDialog::printCurrentItem ); #else @@ -470,6 +488,10 @@ QgsIdentifyResultsDialog::QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidge mActionHideDerivedAttributes->setChecked( mySettings.value( QStringLiteral( "Map/hideDerivedAttributes" ), false ).toBool() ); settingsMenu->addAction( mActionHideNullValues ); mActionHideNullValues->setChecked( QgsIdentifyResultsDialog::settingHideNullValues->value() ); + settingsMenu->addAction( mActionShowReferencedRelations ); + mActionShowReferencedRelations->setChecked( QgsIdentifyResultsDialog::settingShowReferencedRelations->value() ); + settingsMenu->addAction( mActionShowReferencingRelations ); + mActionShowReferencingRelations->setChecked( QgsIdentifyResultsDialog::settingShowReferencingRelations->value() ); } @@ -580,7 +602,7 @@ void QgsIdentifyResultsDialog::addFeature( QgsVectorLayer *vlayer, const QgsFeat connect( vlayer, &QgsVectorLayer::editingStopped, this, &QgsIdentifyResultsDialog::editingToggled ); } - QgsIdentifyResultsFeatureItem *featItem = createFeatureItem( vlayer, f, derivedAttributes, true, layItem ); + QgsIdentifyResultsFeatureItem *featItem = createFeatureItem( vlayer, f, derivedAttributes, layItem ); featItem->setData( 0, Qt::UserRole + 1, mFeatures.size() ); mFeatures << f; layItem->setFirstColumnSpanned( true ); @@ -649,7 +671,28 @@ void QgsIdentifyResultsDialog::addFeature( QgsVectorLayer *vlayer, const QgsFeat highlightFeature( featItem ); } -QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsVectorLayer *vlayer, const QgsFeature &f, const QMap &derivedAttributes, bool includeRelations, QTreeWidgetItem *parentItem ) +/** + * Given an item, returns if the provided feature matches the item or one of + * its ancestor in the tree + */ +/* static */ +bool QgsIdentifyResultsDialog::isFeatureInAncestors( QTreeWidgetItem *item, const QgsVectorLayer *vlayer, const QgsFeature &f ) +{ + for ( ; item; item = item->parent() ) + { + if ( item->data( 0, FeatureRole ).isValid() && item->parent() && vectorLayer( item->parent() ) == vlayer ) + { + const QgsFeature otherF = item->data( 0, FeatureRole ).value< QgsFeature >(); + if ( f.id() == otherF.id() ) + { + return true; + } + } + } + return false; +} + +QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsVectorLayer *vlayer, const QgsFeature &f, const QMap &derivedAttributes, QTreeWidgetItem *parentItem ) { QgsIdentifyResultsFeatureItem *featItem = new QgsIdentifyResultsFeatureItem( vlayer->fields(), f, vlayer->crs() ); featItem->setData( 0, Qt::UserRole, FID_TO_STRING( f.id() ) ); @@ -748,6 +791,7 @@ QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsV const QString originalValue = defVal == attrs.at( i ) ? defVal : fields.at( i ).displayString( attrs.at( i ) ); QgsTreeWidgetItem *attrItem = new QgsTreeWidgetItem( QStringList() << QString::number( i ) << originalValue ); + featItem->addChild( attrItem ); attrItem->setData( 0, Qt::DisplayRole, vlayer->attributeDisplayName( i ) ); @@ -805,8 +849,8 @@ QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsV } } - // add entries for related items - if ( includeRelations ) + // add entries for related items coming from referenced relations + if ( QgsIdentifyResultsDialog::settingShowReferencedRelations->value() ) { const QList relations = QgsProject::instance()->relationManager()->referencedRelations( vlayer ); if ( !relations.empty() ) @@ -815,26 +859,44 @@ QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsV { QgsFeatureIterator childIt = relation.getRelatedFeatures( f ); QgsFeature childFeature; - QgsTreeWidgetItem *relationItem = nullptr; - while ( childIt.nextFeature( childFeature ) ) + if ( childIt.nextFeature( childFeature ) && !isFeatureInAncestors( parentItem, relation.referencingLayer(), childFeature ) ) { - if ( !relationItem ) - { - relationItem = new QgsTreeWidgetItem( QStringList() << relation.name() ); - QFont italicFont; - italicFont.setItalic( true ); - relationItem->setFont( 0, italicFont ); - relationItem->setData( 0, Qt::UserRole, QVariant::fromValue( qobject_cast( relation.referencingLayer() ) ) ); - featItem->addChild( relationItem ); - } - - QgsIdentifyResultsFeatureItem *childItem = createFeatureItem( relation.referencingLayer(), childFeature, QMap(), false, relationItem ); - relationItem->addChild( childItem ); + QgsIdentifyResultsRelationItem *relationItem = new QgsIdentifyResultsRelationItem( QStringList() << relation.name(), relation, true, f ); + QFont italicFont; + italicFont.setItalic( true ); + relationItem->setFont( 0, italicFont ); + relationItem->setData( 0, Qt::UserRole, QVariant::fromValue( qobject_cast( relation.referencingLayer() ) ) ); + relationItem->setText( 0, tr( "%1 through %2 [%3]" ).arg( relation.referencingLayer()->name() ).arg( relation.name() ).arg( childIt.nextFeature( childFeature ) ? "…" : "1" ) ); + relationItem->setChildIndicatorPolicy( QTreeWidgetItem::ShowIndicator ); + featItem->addChild( relationItem ); + // setFirstColumnSpanned() to be done after addChild() to be effective + relationItem->setFirstColumnSpanned( true ); } + } + } + } - if ( relationItem ) + // add entries for related items coming from referencing relations + if ( QgsIdentifyResultsDialog::settingShowReferencingRelations->value() ) + { + const QList relations = QgsProject::instance()->relationManager()->referencingRelations( vlayer ); + if ( !relations.empty() ) + { + for ( const QgsRelation &relation : relations ) + { + QgsFeature parentFeature = relation.getReferencedFeature( f ); + if ( parentFeature.isValid() && !isFeatureInAncestors( parentItem, relation.referencedLayer(), parentFeature ) ) { - relationItem->setText( 0, QStringLiteral( "%1 [%2]" ).arg( relation.name() ).arg( relationItem->childCount() ) ); + QgsIdentifyResultsRelationItem *relationItem = new QgsIdentifyResultsRelationItem( QStringList() << relation.name(), relation, false, f ); + QFont italicFont; + italicFont.setItalic( true ); + relationItem->setFont( 0, italicFont ); + relationItem->setData( 0, Qt::UserRole, QVariant::fromValue( qobject_cast( relation.referencedLayer() ) ) ); + relationItem->setText( 0, tr( "%1 through %2 [%3]" ).arg( relation.referencedLayer()->name() ).arg( relation.name() ) .arg( 1 ) ) ; + relationItem->setChildIndicatorPolicy( QTreeWidgetItem::ShowIndicator ); + featItem->addChild( relationItem ); + // setFirstColumnSpanned() to be done after addChild() to be effective + relationItem->setFirstColumnSpanned( true ); } } } @@ -1506,7 +1568,7 @@ void QgsIdentifyResultsDialog::show() // expand all if enabled if ( mExpandNewAction->isChecked() ) { - lstResults->expandAll(); + expandAll(); } QDialog::show(); @@ -1544,6 +1606,48 @@ void QgsIdentifyResultsDialog::itemClicked( QTreeWidgetItem *item, int column ) } } +void QgsIdentifyResultsDialog::itemExpanded( QTreeWidgetItem *item ) +{ + QgsIdentifyResultsRelationItem *relationItem = dynamic_cast( item ); + if ( relationItem && item->childCount() == 0 ) + { + const QgsRelation &relation = relationItem->relation(); + const QgsFeature &feat = relationItem->topFeature(); + + if ( relationItem->isReferencedRole() ) + { + QgsFeatureIterator childIt = relation.getRelatedFeatures( feat ); + QgsFeature childFeature; + while ( childIt.nextFeature( childFeature ) ) + { + if ( !isFeatureInAncestors( relationItem, relation.referencingLayer(), childFeature ) ) + { + QgsIdentifyResultsFeatureItem *childItem = createFeatureItem( relation.referencingLayer(), childFeature, QMap(), relationItem ); + relationItem->addChild( childItem ); + } + } + + if ( relationItem->childCount() > 1 ) + { + relationItem->setText( 0, tr( "%1 through %2 [%3]" ).arg( relation.referencingLayer()->name() ).arg( relation.name() ).arg( relationItem->childCount() ) ); + } + } + else + { + QgsFeature parentFeature = relation.getReferencedFeature( feat ); + QgsIdentifyResultsFeatureItem *childItem = createFeatureItem( relation.referencedLayer(), parentFeature, QMap(), relationItem ); + relationItem->addChild( childItem ); + } + } + + if ( relationItem && item->childCount() == 1 ) + { + // Small usability: when expanding a relation that has only one related + // feature, expand the feature. + lstResults->expandItem( item->child( 0 ) ); + } +} + // Popup (create if necessary) a context menu that contains a list of // actions that can be applied to the data in the identify results // dialog box. @@ -1591,6 +1695,10 @@ void QgsIdentifyResultsDialog::contextMenuEvent( QContextMenuEvent *event ) if ( featItem->feature().isValid() ) { mActionPopup->addAction( tr( "Zoom to Feature" ), this, &QgsIdentifyResultsDialog::zoomToFeature ); + if ( vlayer && dynamic_cast( featItem->parent() ) ) + { + mActionPopup->addAction( tr( "Explore Feature" ), this, &QgsIdentifyResultsDialog::exploreFeature ); + } mActionPopup->addAction( tr( "Copy Feature" ), this, &QgsIdentifyResultsDialog::copyFeature ); if ( vlayer ) { @@ -2281,6 +2389,22 @@ void QgsIdentifyResultsDialog::zoomToFeature() mCanvas->refresh(); } +void QgsIdentifyResultsDialog::exploreFeature() +{ + QTreeWidgetItem *item = lstResults->currentItem(); + QgsVectorLayer *vlayer = QgsIdentifyResultsDialog::vectorLayer( item ); + if ( !vlayer ) + return; + + QgsIdentifyResultsFeatureItem *featItem = dynamic_cast( featureItem( item ) ); + if ( !featItem ) + return; + + const QgsFeature feat = featItem->feature(); + lstResults->clear(); + addFeature( vlayer, feat, QMap() ); +} + void QgsIdentifyResultsDialog::featureForm() { QTreeWidgetItem *item = lstResults->currentItem(); @@ -2369,7 +2493,16 @@ void QgsIdentifyResultsDialog::layerProperties( QTreeWidgetItem *item ) void QgsIdentifyResultsDialog::expandAll() { - lstResults->expandAll(); + // We can't use expandAll() as it would result in resolving nodes corresponding + // to related features, that we want the user to explicitly expand, to avoid + // creating a potential deeply nested tree. + + QTreeWidgetItemIterator it( lstResults ); + for ( ; *it ; ++it ) + { + if ( ( *it )->childCount() ) + lstResults->expandItem( *it ); + } } void QgsIdentifyResultsDialog::collapseAll() @@ -2518,6 +2651,16 @@ void QgsIdentifyResultsDialog::mActionHideNullValues_toggled( bool checked ) QgsIdentifyResultsDialog::settingHideNullValues->setValue( checked ); } +void QgsIdentifyResultsDialog::mActionShowReferencedRelations_toggled( bool checked ) +{ + QgsIdentifyResultsDialog::settingShowReferencedRelations->setValue( checked ); +} + +void QgsIdentifyResultsDialog::mActionShowReferencingRelations_toggled( bool checked ) +{ + QgsIdentifyResultsDialog::settingShowReferencingRelations->setValue( checked ); +} + void QgsIdentifyResultsDialog::mExpandNewAction_triggered( bool checked ) { QgsSettings settings; diff --git a/src/app/qgsidentifyresultsdialog.h b/src/app/qgsidentifyresultsdialog.h index 4f3c1619ae08..1408b3408339 100644 --- a/src/app/qgsidentifyresultsdialog.h +++ b/src/app/qgsidentifyresultsdialog.h @@ -28,6 +28,7 @@ #include "qgswebview.h" #include "qgsexpressioncontext.h" #include "qgsmaptoolselectionhandler.h" +#include "qgsrelation.h" #include #include @@ -86,6 +87,31 @@ class APP_EXPORT QgsIdentifyResultsFeatureItem: public QTreeWidgetItem QgsCoordinateReferenceSystem mCrs; }; +//! Tree widget item being the parent item of a referenced or referencing relation +class APP_EXPORT QgsIdentifyResultsRelationItem: public QTreeWidgetItem +{ + public: + //! Constructor + QgsIdentifyResultsRelationItem( const QStringList &strings, const QgsRelation &relation, bool isReferencedRole, const QgsFeature &topFeature ); + + //! Return the relation + const QgsRelation &relation() const { return mRelation; } + + /** + * Return true if getRelatedFeatures(mTopFeature) should be called on mRelation, + * or false if getReferencedFeature(mTopFeature) should be called. + */ + bool isReferencedRole() const { return mIsReferencedRole; } + + //! Return the feature that is the parent of this item. + const QgsFeature &topFeature() const { return mTopFeature; } + + private: + QgsRelation mRelation; + bool mIsReferencedRole; + QgsFeature mTopFeature; +}; + class APP_EXPORT QgsIdentifyResultsWebViewItem: public QObject, public QTreeWidgetItem { Q_OBJECT @@ -127,15 +153,15 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti public: /** - * Constructor - - * takes its own copy of the QgsAttributeAction so - * that it is independent of whoever created it. + * Constructor */ QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags() ); ~QgsIdentifyResultsDialog() override; static const QgsSettingsEntryBool *settingHideNullValues; + static const QgsSettingsEntryBool *settingShowReferencingRelations; + static const QgsSettingsEntryBool *settingShowReferencedRelations; //! Adds feature from vector layer void addFeature( QgsVectorLayer *layer, @@ -241,6 +267,7 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti void featureForm(); void zoomToFeature(); + void exploreFeature(); void copyAttributeValue(); void copyFeature(); void toggleFeatureSelection(); @@ -260,6 +287,8 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti /* Item in tree was clicked */ void itemClicked( QTreeWidgetItem *lvi, int column ); + void itemExpanded( QTreeWidgetItem *item ); + QgsAttributeMap retrieveAttributes( QTreeWidgetItem *item ); QVariant retrieveAttribute( QTreeWidgetItem *item ); @@ -275,6 +304,10 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti void mActionHideNullValues_toggled( bool checked ); + void mActionShowReferencingRelations_toggled( bool checked ); + + void mActionShowReferencedRelations_toggled( bool checked ); + void mExpandAction_triggered( bool checked ) { Q_UNUSED( checked ) expandAll(); } void mCollapseAction_triggered( bool checked ) { Q_UNUSED( checked ) collapseAll(); } @@ -306,7 +339,7 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti QToolButton *mSelectModeButton = nullptr; QgsMapLayer *layer( QTreeWidgetItem *item ); - QgsVectorLayer *vectorLayer( QTreeWidgetItem *item ); + static QgsVectorLayer *vectorLayer( QTreeWidgetItem *item ); QgsRasterLayer *rasterLayer( QTreeWidgetItem *item ); QgsMeshLayer *meshLayer( QTreeWidgetItem *item ); QgsVectorTileLayer *vectorTileLayer( QTreeWidgetItem *item ); @@ -341,9 +374,12 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti void setSelectionMode(); void initSelectionModes(); - QgsIdentifyResultsFeatureItem *createFeatureItem( QgsVectorLayer *vlayer, const QgsFeature &f, const QMap &derivedAttributes, bool includeRelations, QTreeWidgetItem *parentItem ); + QgsIdentifyResultsFeatureItem *createFeatureItem( QgsVectorLayer *vlayer, const QgsFeature &f, const QMap &derivedAttributes, QTreeWidgetItem *parentItem ); + + static bool isFeatureInAncestors( QTreeWidgetItem *item, const QgsVectorLayer *vlayer, const QgsFeature &f ); friend class TestQgsMapToolIdentifyAction; + friend class TestQgsIdentifyResultsDialog; }; class QgsIdentifyResultsDialogMapLayerAction : public QAction diff --git a/src/ui/qgsidentifyresultsbase.ui b/src/ui/qgsidentifyresultsbase.ui index 23591c78187f..568797d29478 100644 --- a/src/ui/qgsidentifyresultsbase.ui +++ b/src/ui/qgsidentifyresultsbase.ui @@ -383,6 +383,28 @@ Hide derived attributes from results + + + true + + + Show referencing relations + + + Show referencing relations + + + + + true + + + Show referenced relations + + + Show referenced relations + + diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index 333cd25ff0d9..1bf5581c389c 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -19,6 +19,7 @@ set(TESTS testqgsdecorationscalebar.cpp testqgsdwgimportdialog.cpp testqgsfieldcalculator.cpp + testqgsidentifyresultsdialog.cpp testqgslayerpropertiesdialogs.cpp testqgsmapcanvasdockwidget.cpp testqgsmaptooladdpart.cpp diff --git a/tests/src/app/testqgsidentifyresultsdialog.cpp b/tests/src/app/testqgsidentifyresultsdialog.cpp new file mode 100644 index 000000000000..27b8a5059917 --- /dev/null +++ b/tests/src/app/testqgsidentifyresultsdialog.cpp @@ -0,0 +1,270 @@ +/*************************************************************************** + testqgsidentifyresultsdialog.cpp + -------------------------------- + Date : 2024-06-21 + Copyright : (C) 2024 by Even Rouault + Email : even.rouault at spatialys.com + *************************************************************************** + * * + * 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 "qgstest.h" + +#include "qgisapp.h" +#include "qgsapplication.h" +#include "qgsidentifyresultsdialog.h" +#include "qgsmapcanvas.h" +#include "qgsproject.h" +#include "qgsrelation.h" +#include "qgsrelationmanager.h" +#include "qgsvectorlayer.h" + +class TestQgsIdentifyResultsDialog : public QObject +{ + Q_OBJECT + public: + TestQgsIdentifyResultsDialog() = default; + + private slots: + void initTestCase(); // will be called before the first testfunction is executed. + void cleanupTestCase(); // will be called after the last testfunction was executed. + void init(); // will be called before each testfunction is executed. + void cleanup(); // will be called after every testfunction. + + void testRelations(); + + private: + + QgsMapCanvas *mCanvas = nullptr; + QgisApp *mQgisApp = nullptr; +}; + +void TestQgsIdentifyResultsDialog::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + // Set up the QgsSettings environment + QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) ); + QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) ); + QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST" ) ); + + QgsApplication::showSettings(); + + // enforce C locale because the tests expect it + // (decimal separators / thousand separators) + QLocale::setDefault( QLocale::c() ); + + mQgisApp = new QgisApp(); +} + +void TestQgsIdentifyResultsDialog::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsIdentifyResultsDialog::init() +{ + mCanvas = new QgsMapCanvas(); +} + +void TestQgsIdentifyResultsDialog::cleanup() +{ + delete mCanvas; +} + +void TestQgsIdentifyResultsDialog::testRelations() +{ + QgsVectorLayer *layerA = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:4326&field=pk_id:integer" ), QStringLiteral( "layerA" ), QStringLiteral( "memory" ) ); + QVERIFY( layerA->isValid() ); + QgsFeature featureA( layerA->dataProvider()->fields() ); + constexpr int PK_ID_A = 1; + constexpr int PK_ID_C = 2; + featureA.setAttribute( 0, PK_ID_A ); + layerA->dataProvider()->addFeature( featureA ); + + QgsVectorLayer *layerB = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:4326&field=fk_id_to_A:integer&field=fk_id_to_C:integer&field=other_field:integer" ), QStringLiteral( "layerB" ), QStringLiteral( "memory" ) ); + QVERIFY( layerB->isValid() ); + constexpr int IDX_OTHER_FIELD = 2; + constexpr int OTHER_FIELD = 100; + { + QgsFeature featureB( layerB->dataProvider()->fields() ); + featureB.setAttribute( 0, PK_ID_A ); + featureB.setAttribute( 1, PK_ID_C ); + featureB.setAttribute( IDX_OTHER_FIELD, OTHER_FIELD ); + layerB->dataProvider()->addFeature( featureB ); + } + { + QgsFeature featureB( layerB->dataProvider()->fields() ); + featureB.setAttribute( 0, PK_ID_A ); + featureB.setAttribute( 1, PK_ID_C + 1 ); + featureB.setAttribute( IDX_OTHER_FIELD, OTHER_FIELD + 1 ); + layerB->dataProvider()->addFeature( featureB ); + } + + QgsVectorLayer *layerC = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:4326&field=pk_id:integer" ), QStringLiteral( "layerC" ), QStringLiteral( "memory" ) ); + QVERIFY( layerC->isValid() ); + { + QgsFeature featureC( layerC->dataProvider()->fields() ); + featureC.setAttribute( 0, PK_ID_C ); + layerC->dataProvider()->addFeature( featureC ); + } + { + QgsFeature featureC( layerC->dataProvider()->fields() ); + featureC.setAttribute( 0, PK_ID_C + 1 ); + layerC->dataProvider()->addFeature( featureC ); + } + + QgsProject::instance()->layerStore()->addMapLayer( layerA, true ); + QgsProject::instance()->layerStore()->addMapLayer( layerB, true ); + QgsProject::instance()->layerStore()->addMapLayer( layerC, true ); + + QgsRelationManager *relationManager = QgsProject::instance()->relationManager(); + { + QgsRelation relation; + relation.setId( "B-A-id" ); + relation.setName( "B-A" ); + relation.setReferencingLayer( layerB->id() ); + relation.setReferencedLayer( layerA->id() ); + relation.addFieldPair( QStringLiteral( "fk_id_to_A" ), QStringLiteral( "pk_id" ) ); + + relationManager->addRelation( relation ); + } + { + QgsRelation relation; + relation.setId( "A-B-id" ); + relation.setName( "A-B" ); + relation.setReferencingLayer( layerA->id() ); + relation.setReferencedLayer( layerB->id() ); + relation.addFieldPair( QStringLiteral( "pk_id" ), QStringLiteral( "fk_id_to_A" ) ); + + relationManager->addRelation( relation ); + } + { + QgsRelation relation; + relation.setId( "B-C-id" ); + relation.setName( "B-C" ); + relation.setReferencingLayer( layerB->id() ); + relation.setReferencedLayer( layerC->id() ); + relation.addFieldPair( QStringLiteral( "fk_id_to_C" ), QStringLiteral( "pk_id" ) ); + + relationManager->addRelation( relation ); + } + + std::unique_ptr dialog = std::make_unique( mCanvas ); + dialog->addFeature( layerA, featureA, QMap< QString, QString>() ); + + QCOMPARE( dialog->lstResults->topLevelItemCount(), 1 ); + QTreeWidgetItem *topLevelItem = dialog->lstResults->topLevelItem( 0 ); + QCOMPARE( topLevelItem->childCount(), 1 ); + QgsIdentifyResultsFeatureItem *featureItem = dynamic_cast( topLevelItem->child( 0 ) ); + QVERIFY( featureItem ); + std::vector relationItems; + for ( int i = 0; i < featureItem->childCount(); ++i ) + { + QgsIdentifyResultsRelationItem *relationItem = dynamic_cast( featureItem->child( i ) ); + if ( relationItem ) + relationItems.push_back( relationItem ); + } + QCOMPARE( relationItems.size(), 2 ); + + QCOMPARE( relationItems[0]->text( 0 ), QStringLiteral( "layerB through B-A […]" ) ); + QCOMPARE( relationItems[0]->childCount(), 0 ); + QCOMPARE( relationItems[0]->childIndicatorPolicy(), QTreeWidgetItem::ShowIndicator ); + QCOMPARE( relationItems[0]->isExpanded(), false ); + + QCOMPARE( relationItems[1]->text( 0 ), QStringLiteral( "layerB through A-B [1]" ) ); + QCOMPARE( relationItems[1]->childCount(), 0 ); + QCOMPARE( relationItems[1]->childIndicatorPolicy(), QTreeWidgetItem::ShowIndicator ); + QCOMPARE( relationItems[1]->isExpanded(), false ); + + // Check referenced relation + + // Check that expandAll() doesn't result in automatic resolution of relations + dialog->expandAll(); + QCOMPARE( relationItems[0]->childCount(), 0 ); + + relationItems[0]->setExpanded( true ); + QCOMPARE( relationItems[0]->text( 0 ), QStringLiteral( "layerB through B-A [2]" ) ); + QCOMPARE( relationItems[0]->childCount(), 2 ); + + // Check that folding/unfolding after initial expansion works + relationItems[0]->setExpanded( false ); + relationItems[0]->setExpanded( true ); + QCOMPARE( relationItems[0]->text( 0 ), QStringLiteral( "layerB through B-A [2]" ) ); + QCOMPARE( relationItems[0]->childCount(), 2 ); + + { + QgsIdentifyResultsFeatureItem *relatedFeatureItem = dynamic_cast( relationItems[0]->child( 0 ) ); + QVERIFY( relatedFeatureItem ); + QVERIFY( relatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).isValid() ); + const QgsFeature relatedFeature = relatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).value< QgsFeature >(); + QCOMPARE( relatedFeature.attribute( IDX_OTHER_FIELD ), OTHER_FIELD ); + + { + std::vector childRelationItems; + for ( int i = 0; i < relatedFeatureItem->childCount(); ++i ) + { + QgsIdentifyResultsRelationItem *relationItem = dynamic_cast( relatedFeatureItem->child( i ) ); + if ( relationItem ) + childRelationItems.push_back( relationItem ); + } + QCOMPARE( childRelationItems.size(), 1 ); + + QCOMPARE( childRelationItems[0]->childCount(), 0 ); + QCOMPARE( childRelationItems[0]->text( 0 ), QStringLiteral( "layerC through B-C [1]" ) ); + + childRelationItems[0]->setExpanded( true ); + QCOMPARE( childRelationItems[0]->childCount(), 1 ); + QCOMPARE( childRelationItems[0]->text( 0 ), QStringLiteral( "layerC through B-C [1]" ) ); + + { + QgsIdentifyResultsFeatureItem *childRelatedFeatureItem = dynamic_cast( childRelationItems[0]->child( 0 ) ); + QVERIFY( childRelatedFeatureItem ); + QVERIFY( childRelatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).isValid() ); + const QgsFeature relatedFeature = childRelatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).value< QgsFeature >(); + QCOMPARE( relatedFeature.attribute( 0 ), PK_ID_C ); + + // Check that this child doesn't link back to parent feature A + std::vector childChildRelationItems; + for ( int i = 0; i < childRelatedFeatureItem->childCount(); ++i ) + { + QgsIdentifyResultsRelationItem *relationItem = dynamic_cast( childRelatedFeatureItem->child( i ) ); + if ( relationItem ) + childChildRelationItems.push_back( relationItem ); + } + QCOMPARE( childChildRelationItems.size(), 0 ); + } + } + + } + + { + QgsIdentifyResultsFeatureItem *relatedFeatureItem = dynamic_cast( relationItems[0]->child( 1 ) ); + QVERIFY( relatedFeatureItem ); + QVERIFY( relatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).isValid() ); + const QgsFeature relatedFeature = relatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).value< QgsFeature >(); + QCOMPARE( relatedFeature.attribute( IDX_OTHER_FIELD ), OTHER_FIELD + 1 ); + } + + // Check referencing relation + relationItems[1]->setExpanded( true ); + QCOMPARE( relationItems[1]->text( 0 ), QStringLiteral( "layerB through A-B [1]" ) ); + QCOMPARE( relationItems[1]->childCount(), 1 ); + + { + QgsIdentifyResultsFeatureItem *relatedFeatureItem = dynamic_cast( relationItems[1]->child( 0 ) ); + QVERIFY( relatedFeatureItem ); + QVERIFY( relatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).isValid() ); + const QgsFeature relatedFeature = relatedFeatureItem->data( 0, QgsIdentifyResultsDialog::FeatureRole ).value< QgsFeature >(); + QCOMPARE( relatedFeature.attribute( IDX_OTHER_FIELD ), OTHER_FIELD ); + } +} + + +QGSTEST_MAIN( TestQgsIdentifyResultsDialog ) +#include "testqgsidentifyresultsdialog.moc"