Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] [Identify tool] Present referencing relations (in addition … #57863

Merged
merged 5 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 182 additions & 24 deletions src/app/qgsidentifyresultsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <QLabel>
#include <QAction>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
#include <QPixmap>
#include <QMenu>
#include <QClipboard>
Expand Down Expand Up @@ -107,6 +108,8 @@ 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::settingShowRelations = new QgsSettingsEntryBool( QStringLiteral( "show-relations" ), QgsSettingsTree::sTreeMap, true, QStringLiteral( "Whether to show relations in the identify feature result" ) );


QgsIdentifyResultsWebView::QgsIdentifyResultsWebView( QWidget *parent ) : QgsWebView( parent )
{
Expand Down Expand Up @@ -295,6 +298,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 );
Expand Down Expand Up @@ -361,6 +372,7 @@ 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( mActionShowRelations, &QAction::toggled, this, &QgsIdentifyResultsDialog::mActionShowRelations_toggled );

mOpenFormAction->setDisabled( true );

Expand Down Expand Up @@ -442,6 +454,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
Expand Down Expand Up @@ -470,6 +485,8 @@ QgsIdentifyResultsDialog::QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidge
mActionHideDerivedAttributes->setChecked( mySettings.value( QStringLiteral( "Map/hideDerivedAttributes" ), false ).toBool() );
settingsMenu->addAction( mActionHideNullValues );
mActionHideNullValues->setChecked( QgsIdentifyResultsDialog::settingHideNullValues->value() );
settingsMenu->addAction( mActionShowRelations );
mActionShowRelations->setChecked( QgsIdentifyResultsDialog::settingShowRelations->value() );

}

Expand Down Expand Up @@ -580,7 +597,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 );
Expand Down Expand Up @@ -649,7 +666,28 @@ void QgsIdentifyResultsDialog::addFeature( QgsVectorLayer *vlayer, const QgsFeat
highlightFeature( featItem );
}

QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsVectorLayer *vlayer, const QgsFeature &f, const QMap<QString, QString> &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<QString, QString> &derivedAttributes, QTreeWidgetItem *parentItem )
{
QgsIdentifyResultsFeatureItem *featItem = new QgsIdentifyResultsFeatureItem( vlayer->fields(), f, vlayer->crs() );
featItem->setData( 0, Qt::UserRole, FID_TO_STRING( f.id() ) );
Expand Down Expand Up @@ -748,6 +786,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 ) );
Expand Down Expand Up @@ -805,8 +844,8 @@ QgsIdentifyResultsFeatureItem *QgsIdentifyResultsDialog::createFeatureItem( QgsV
}
}

// add entries for related items
if ( includeRelations )
// add entries for related items coming from referenced relations
if ( QgsIdentifyResultsDialog::settingShowRelations->value() )
{
const QList<QgsRelation> relations = QgsProject::instance()->relationManager()->referencedRelations( vlayer );
if ( !relations.empty() )
Expand All @@ -815,26 +854,48 @@ 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<QObject *>( relation.referencingLayer() ) ) );
featItem->addChild( relationItem );
}

QgsIdentifyResultsFeatureItem *childItem = createFeatureItem( relation.referencingLayer(), childFeature, QMap<QString, QString>(), 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<QObject *>( 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 );

connect( relation.referencingLayer(), &QObject::destroyed, this, &QgsIdentifyResultsDialog::layerDestroyed );
}
}
}
}

if ( relationItem )
// add entries for related items coming from referencing relations
if ( QgsIdentifyResultsDialog::settingShowRelations->value() )
{
const QList<QgsRelation> 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<QObject *>( relation.referencedLayer() ) ) );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks dangerous -- do we do this elsewhere? I think it'd be safer to use the layer id here instead . Otherwise there could be a crash if the child layer is removed before the relation is expanded

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks dangerous -- do we do this elsewhere?

yes, for example

layItem->setData( 0, Qt::UserRole, QVariant::fromValue( qobject_cast<QObject *>( vlayer ) ) );
. But they also connected to QgsIdentifyResultsDialog::layerDestroyed() to remove nodes from the tree, which was missing for the relations. I've added this connection and enhanced layerDestroyed() to remove items that are linked to a layer, but not necessarily as top items.

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 );

connect( relation.referencedLayer(), &QObject::destroyed, this, &QgsIdentifyResultsDialog::layerDestroyed );
}
}
}
Expand Down Expand Up @@ -1506,7 +1567,7 @@ void QgsIdentifyResultsDialog::show()
// expand all if enabled
if ( mExpandNewAction->isChecked() )
{
lstResults->expandAll();
expandAll();
}

QDialog::show();
Expand Down Expand Up @@ -1544,6 +1605,48 @@ void QgsIdentifyResultsDialog::itemClicked( QTreeWidgetItem *item, int column )
}
}

void QgsIdentifyResultsDialog::itemExpanded( QTreeWidgetItem *item )
{
QgsIdentifyResultsRelationItem *relationItem = dynamic_cast<QgsIdentifyResultsRelationItem *>( 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<QString, QString>(), 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<QString, QString>(), 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.
Expand Down Expand Up @@ -1591,6 +1694,10 @@ void QgsIdentifyResultsDialog::contextMenuEvent( QContextMenuEvent *event )
if ( featItem->feature().isValid() )
{
mActionPopup->addAction( tr( "Zoom to Feature" ), this, &QgsIdentifyResultsDialog::zoomToFeature );
if ( vlayer && dynamic_cast<QgsIdentifyResultsRelationItem *>( featItem->parent() ) )
{
mActionPopup->addAction( tr( "Identify Feature" ), this, &QgsIdentifyResultsDialog::identifyFeature );
}
mActionPopup->addAction( tr( "Copy Feature" ), this, &QgsIdentifyResultsDialog::copyFeature );
if ( vlayer )
{
Expand Down Expand Up @@ -2058,6 +2165,21 @@ void QgsIdentifyResultsDialog::handleCurrentItemChanged( QTreeWidgetItem *curren
}
}

static void deleteItemIfBelongingToLayer( QObject *senderObject, QTreeWidgetItem *item )
{
if ( item->data( 0, Qt::UserRole ).value<QObject *>() == senderObject )
{
delete item;
}
else
{
for ( int i = item->childCount() - 1; i >= 0; i-- )
{
deleteItemIfBelongingToLayer( senderObject, item->child( i ) );
}
}
}

void QgsIdentifyResultsDialog::layerDestroyed()
{
QObject *senderObject = sender();
Expand All @@ -2076,9 +2198,15 @@ void QgsIdentifyResultsDialog::layerDestroyed()
}

disconnectLayer( senderObject );
delete layerItem( senderObject );

// remove items, starting from last
// remove items from the tree that are related to the layer
for ( int i = lstResults->topLevelItemCount() - 1; i >= 0; i-- )
{
QTreeWidgetItem *item = lstResults->topLevelItem( i );
deleteItemIfBelongingToLayer( senderObject, item );
}

// remove items from the table, starting from last
for ( int i = tblResults->rowCount() - 1; i >= 0; i-- )
{
QgsDebugMsgLevel( QStringLiteral( "item %1 / %2" ).arg( i ).arg( tblResults->rowCount() ), 3 );
Expand Down Expand Up @@ -2281,6 +2409,22 @@ void QgsIdentifyResultsDialog::zoomToFeature()
mCanvas->refresh();
}

void QgsIdentifyResultsDialog::identifyFeature()
{
QTreeWidgetItem *item = lstResults->currentItem();
QgsVectorLayer *vlayer = QgsIdentifyResultsDialog::vectorLayer( item );
if ( !vlayer )
return;

QgsIdentifyResultsFeatureItem *featItem = dynamic_cast<QgsIdentifyResultsFeatureItem *>( featureItem( item ) );
if ( !featItem )
return;

const QgsFeature feat = featItem->feature();
lstResults->clear();
addFeature( vlayer, feat, QMap<QString, QString>() );
}

void QgsIdentifyResultsDialog::featureForm()
{
QTreeWidgetItem *item = lstResults->currentItem();
Expand Down Expand Up @@ -2369,7 +2513,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()
Expand Down Expand Up @@ -2518,6 +2671,11 @@ void QgsIdentifyResultsDialog::mActionHideNullValues_toggled( bool checked )
QgsIdentifyResultsDialog::settingHideNullValues->setValue( checked );
}

void QgsIdentifyResultsDialog::mActionShowRelations_toggled( bool checked )
{
QgsIdentifyResultsDialog::settingShowRelations->setValue( checked );
}

void QgsIdentifyResultsDialog::mExpandNewAction_triggered( bool checked )
{
QgsSettings settings;
Expand Down
Loading
Loading