1 /***************************************************************************
2     qgsrelationreferencewidget.cpp
3      --------------------------------------
4     Date                 : 20.4.2013
5     Copyright            : (C) 2013 Matthias Kuhn
6     Email                : matthias at opengis dot ch
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 #include "qgsrelationreferencewidget.h"
17 
18 #include <QPushButton>
19 #include <QDialog>
20 #include <QHBoxLayout>
21 #include <QTimer>
22 #include <QCompleter>
23 
24 #include "qgsattributeform.h"
25 #include "qgsattributetablefiltermodel.h"
26 #include "qgsattributedialog.h"
27 #include "qgsapplication.h"
28 #include "qgscollapsiblegroupbox.h"
29 #include "qgseditorwidgetfactory.h"
30 #include "qgsexpression.h"
31 #include "qgsfeaturelistmodel.h"
32 #include "qgsfields.h"
33 #include "qgsgeometry.h"
34 #include "qgshighlight.h"
35 #include "qgsmapcanvas.h"
36 #include "qgsmessagebar.h"
37 #include "qgsrelationreferenceconfigdlg.h"
38 #include "qgsvectorlayer.h"
39 #include "qgsattributetablemodel.h"
40 #include "qgsmaptoolidentifyfeature.h"
41 #include "qgsmaptooldigitizefeature.h"
42 #include "qgsfeatureiterator.h"
43 #include "qgsfeaturelistcombobox.h"
44 #include "qgsexpressioncontextutils.h"
45 #include "qgsfeaturefiltermodel.h"
46 #include "qgsidentifymenu.h"
47 #include "qgsvectorlayerutils.h"
48 
49 
qVariantListIsNull(const QVariantList & list)50 bool qVariantListIsNull( const QVariantList &list )
51 {
52   if ( list.isEmpty() )
53     return true;
54 
55   for ( int i = 0; i < list.size(); ++i )
56   {
57     if ( !list.at( i ).isNull() )
58       return false;
59   }
60   return true;
61 }
62 
63 
QgsRelationReferenceWidget(QWidget * parent)64 QgsRelationReferenceWidget::QgsRelationReferenceWidget( QWidget *parent )
65   : QWidget( parent )
66 {
67   mTopLayout = new QVBoxLayout( this );
68   mTopLayout->setContentsMargins( 0, 0, 0, 0 );
69 
70   setSizePolicy( sizePolicy().horizontalPolicy(), QSizePolicy::Fixed );
71 
72   setLayout( mTopLayout );
73 
74   QHBoxLayout *editLayout = new QHBoxLayout();
75   editLayout->setContentsMargins( 0, 0, 0, 0 );
76   editLayout->setSpacing( 2 );
77 
78   // Prepare the container and layout for the filter comboboxes
79   mChooserContainer = new QWidget;
80   editLayout->addWidget( mChooserContainer );
81   QHBoxLayout *chooserLayout = new QHBoxLayout;
82   chooserLayout->setContentsMargins( 0, 0, 0, 0 );
83   mFilterLayout = new QHBoxLayout;
84   mFilterLayout->setContentsMargins( 0, 0, 0, 0 );
85   mFilterContainer = new QWidget;
86   mFilterContainer->setLayout( mFilterLayout );
87   mChooserContainer->setLayout( chooserLayout );
88   chooserLayout->addWidget( mFilterContainer );
89 
90   mComboBox = new QgsFeatureListComboBox();
91   mChooserContainer->layout()->addWidget( mComboBox );
92 
93   // open form button
94   mOpenFormButton = new QToolButton();
95   mOpenFormButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionPropertyItem.svg" ) ) );
96   mOpenFormButton->setText( tr( "Open Related Feature Form" ) );
97   editLayout->addWidget( mOpenFormButton );
98 
99   mAddEntryButton = new QToolButton();
100   mAddEntryButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionAdd.svg" ) ) );
101   mAddEntryButton->setText( tr( "Add New Entry" ) );
102   editLayout->addWidget( mAddEntryButton );
103 
104   // highlight button
105   mHighlightFeatureButton = new QToolButton( this );
106   mHighlightFeatureButton->setPopupMode( QToolButton::MenuButtonPopup );
107   mHighlightFeatureAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionHighlightFeature.svg" ) ), tr( "Highlight feature" ), this );
108   mScaleHighlightFeatureAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionScaleHighlightFeature.svg" ) ), tr( "Scale and highlight feature" ), this );
109   mPanHighlightFeatureAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionPanHighlightFeature.svg" ) ), tr( "Pan and highlight feature" ), this );
110   mHighlightFeatureButton->addAction( mHighlightFeatureAction );
111   mHighlightFeatureButton->addAction( mScaleHighlightFeatureAction );
112   mHighlightFeatureButton->addAction( mPanHighlightFeatureAction );
113   mHighlightFeatureButton->setDefaultAction( mHighlightFeatureAction );
114   editLayout->addWidget( mHighlightFeatureButton );
115 
116   // map identification button
117   mMapIdentificationButton = new QToolButton( this );
118   mMapIdentificationButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMapIdentification.svg" ) ) );
119   mMapIdentificationButton->setText( tr( "Select on Map" ) );
120   mMapIdentificationButton->setCheckable( true );
121   editLayout->addWidget( mMapIdentificationButton );
122 
123   // remove foreign key button
124   mRemoveFKButton = new QToolButton( this );
125   mRemoveFKButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRemove.svg" ) ) );
126   mRemoveFKButton->setText( tr( "No Selection" ) );
127   editLayout->addWidget( mRemoveFKButton );
128 
129   // add line to top layout
130   mTopLayout->addLayout( editLayout );
131 
132   // embed form
133   mAttributeEditorFrame = new QgsCollapsibleGroupBox( this );
134   mAttributeEditorLayout = new QVBoxLayout( mAttributeEditorFrame );
135   mAttributeEditorFrame->setLayout( mAttributeEditorLayout );
136   mAttributeEditorFrame->setSizePolicy( mAttributeEditorFrame->sizePolicy().horizontalPolicy(), QSizePolicy::Expanding );
137   mTopLayout->addWidget( mAttributeEditorFrame );
138 
139   // invalid label
140   mInvalidLabel = new QLabel( tr( "The relation is not valid. Please make sure your relation definitions are OK." ) );
141   mInvalidLabel->setWordWrap( true );
142   QFont font = mInvalidLabel->font();
143   font.setItalic( true );
144   mInvalidLabel->setStyleSheet( QStringLiteral( "QLabel { color: red; } " ) );
145   mInvalidLabel->setFont( font );
146   mTopLayout->addWidget( mInvalidLabel );
147 
148   // default mode is combobox, no geometric relation and no embed form
149   mMapIdentificationButton->hide();
150   mHighlightFeatureButton->hide();
151   mAttributeEditorFrame->hide();
152   mInvalidLabel->hide();
153   mAddEntryButton->hide();
154 
155   // connect buttons
156   connect( mOpenFormButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::openForm );
157   connect( mHighlightFeatureButton, &QToolButton::triggered, this, &QgsRelationReferenceWidget::highlightActionTriggered );
158   connect( mMapIdentificationButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::mapIdentification );
159   connect( mRemoveFKButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::deleteForeignKeys );
160   connect( mAddEntryButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::addEntry );
161   connect( mComboBox, &QComboBox::editTextChanged, this, &QgsRelationReferenceWidget::updateAddEntryButton );
162 }
163 
~QgsRelationReferenceWidget()164 QgsRelationReferenceWidget::~QgsRelationReferenceWidget()
165 {
166   deleteHighlight();
167   unsetMapTool();
168 }
169 
setRelation(const QgsRelation & relation,bool allowNullValue)170 void QgsRelationReferenceWidget::setRelation( const QgsRelation &relation, bool allowNullValue )
171 {
172   mAllowNull = allowNullValue;
173   mRemoveFKButton->setVisible( allowNullValue && mReadOnlySelector );
174 
175   if ( relation.isValid() )
176   {
177     mReferencedLayerId = relation.referencedLayerId();
178     mReferencedLayerName = relation.referencedLayer()->name();
179     setReferencedLayerDataSource( relation.referencedLayer()->publicSource() );
180     mReferencedLayerProviderKey = relation.referencedLayer()->providerType();
181     mInvalidLabel->hide();
182 
183     mRelation = relation;
184     mReferencingLayer = relation.referencingLayer();
185     mReferencedLayer = relation.referencedLayer();
186 
187     const QList<QgsRelation::FieldPair> fieldPairs = relation.fieldPairs();
188     for ( const QgsRelation::FieldPair &fieldPair : fieldPairs )
189     {
190       mReferencedFields << fieldPair.referencedField();
191     }
192     if ( mComboBox )
193     {
194       mComboBox->setAllowNull( mAllowNull );
195       mComboBox->setSourceLayer( mReferencedLayer );
196       mComboBox->setIdentifierFields( mReferencedFields );
197       mComboBox->setFilterExpression( mFilterExpression );
198     }
199     mAttributeEditorFrame->setObjectName( QStringLiteral( "referencing/" ) + relation.name() );
200 
201     if ( mEmbedForm )
202     {
203       QgsAttributeEditorContext context( mEditorContext, relation, QgsAttributeEditorContext::Single, QgsAttributeEditorContext::Embed );
204       mAttributeEditorFrame->setTitle( mReferencedLayer->name() );
205       mReferencedAttributeForm = new QgsAttributeForm( relation.referencedLayer(), QgsFeature(), context, this );
206       mAttributeEditorLayout->addWidget( mReferencedAttributeForm );
207     }
208 
209     connect( mReferencedLayer, &QgsVectorLayer::editingStarted, this, &QgsRelationReferenceWidget::updateAddEntryButton );
210     connect( mReferencedLayer, &QgsVectorLayer::editingStopped, this, &QgsRelationReferenceWidget::updateAddEntryButton );
211     updateAddEntryButton();
212   }
213   else
214   {
215     mInvalidLabel->show();
216   }
217 
218   if ( mShown && isVisible() )
219   {
220     init();
221   }
222 }
223 
setRelationEditable(bool editable)224 void QgsRelationReferenceWidget::setRelationEditable( bool editable )
225 {
226   if ( !editable )
227   {
228     unsetMapTool();
229   }
230 
231   mFilterContainer->setEnabled( editable );
232   mComboBox->setEnabled( editable && !mReadOnlySelector );
233   mComboBox->setEditable( true );
234   mMapIdentificationButton->setEnabled( editable );
235   mRemoveFKButton->setEnabled( editable );
236   mIsEditable = editable;
237 }
238 
setForeignKey(const QVariant & value)239 void QgsRelationReferenceWidget::setForeignKey( const QVariant &value )
240 {
241   setForeignKeys( QVariantList() << value );
242 }
243 
setForeignKeys(const QVariantList & values)244 void QgsRelationReferenceWidget::setForeignKeys( const QVariantList &values )
245 {
246   if ( values.isEmpty() )
247   {
248     return;
249   }
250   if ( qVariantListIsNull( values ) )
251   {
252     deleteForeignKeys();
253     return;
254   }
255 
256   if ( !mReferencedLayer )
257     return;
258 
259   mComboBox->setIdentifierValues( values );
260 
261   if ( mEmbedForm || mChainFilters )
262   {
263     QgsFeatureRequest request = mComboBox->currentFeatureRequest();
264     mReferencedLayer->getFeatures( request ).nextFeature( mFeature );
265   }
266   if ( mChainFilters )
267   {
268     QVariant nullValue = QgsApplication::nullRepresentation();
269     const int count = std::min( mFilterComboBoxes.size(), mFilterFields.size() );
270     for ( int i = 0; i < count; i++ )
271     {
272       QVariant v = mFeature.attribute( mFilterFields[i] );
273       QString f = v.isNull() ? nullValue.toString() : v.toString();
274       mFilterComboBoxes.at( i )->setCurrentIndex( mFilterComboBoxes.at( i )->findText( f ) );
275     }
276   }
277 
278   mRemoveFKButton->setEnabled( mIsEditable );
279   highlightFeature( mFeature ); // TODO : make this async
280   updateAttributeEditorFrame( mFeature );
281 
282   emitForeignKeysChanged( foreignKeys() );
283 }
284 
deleteForeignKeys()285 void QgsRelationReferenceWidget::deleteForeignKeys()
286 {
287   // deactivate filter comboboxes
288   if ( mChainFilters && !mFilterComboBoxes.isEmpty() )
289   {
290     QComboBox *cb = mFilterComboBoxes.first();
291     cb->setCurrentIndex( 0 );
292     disableChainedComboBoxes( cb );
293   }
294 
295   mComboBox->setIdentifierValuesToNull();
296   mRemoveFKButton->setEnabled( false );
297   updateAttributeEditorFrame( QgsFeature() );
298 
299   emitForeignKeysChanged( foreignKeys() );
300 }
301 
referencedFeature() const302 QgsFeature QgsRelationReferenceWidget::referencedFeature() const
303 {
304   QgsFeature f;
305   if ( mReferencedLayer )
306   {
307     mReferencedLayer->getFeatures( mComboBox->currentFeatureRequest() ).nextFeature( f );
308   }
309   return f;
310 }
311 
showIndeterminateState()312 void QgsRelationReferenceWidget::showIndeterminateState()
313 {
314   whileBlocking( mComboBox )->setIdentifierValuesToNull();
315   mRemoveFKButton->setEnabled( false );
316   updateAttributeEditorFrame( QgsFeature() );
317 }
318 
foreignKey() const319 QVariant QgsRelationReferenceWidget::foreignKey() const
320 {
321   QVariantList fkeys;
322   if ( fkeys.isEmpty() )
323     return QVariant( QVariant::Int );
324   else
325     return fkeys.at( 0 );
326 }
327 
foreignKeys() const328 QVariantList QgsRelationReferenceWidget::foreignKeys() const
329 {
330   return mComboBox->identifierValues();
331 }
332 
setEditorContext(const QgsAttributeEditorContext & context,QgsMapCanvas * canvas,QgsMessageBar * messageBar)333 void QgsRelationReferenceWidget::setEditorContext( const QgsAttributeEditorContext &context, QgsMapCanvas *canvas, QgsMessageBar *messageBar )
334 {
335   mEditorContext = context;
336   mCanvas = canvas;
337   mMessageBar = messageBar;
338 
339   mMapToolIdentify.reset( new QgsMapToolIdentifyFeature( mCanvas ) );
340   mMapToolIdentify->setButton( mMapIdentificationButton );
341 
342   if ( mEditorContext.cadDockWidget() )
343   {
344     mMapToolDigitize.reset( new QgsMapToolDigitizeFeature( mCanvas, mEditorContext.cadDockWidget() ) );
345     mMapToolDigitize->setButton( mAddEntryButton );
346     updateAddEntryButton();
347   }
348 }
349 
setEmbedForm(bool display)350 void QgsRelationReferenceWidget::setEmbedForm( bool display )
351 {
352   if ( display )
353   {
354     setSizePolicy( sizePolicy().horizontalPolicy(), QSizePolicy::MinimumExpanding );
355     mTopLayout->setAlignment( Qt::AlignTop );
356   }
357 
358   mAttributeEditorFrame->setVisible( display );
359   mEmbedForm = display;
360 }
361 
setReadOnlySelector(bool readOnly)362 void QgsRelationReferenceWidget::setReadOnlySelector( bool readOnly )
363 {
364   mComboBox->setEnabled( !readOnly );
365   mRemoveFKButton->setVisible( mAllowNull && readOnly );
366   mReadOnlySelector = readOnly;
367 }
368 
setAllowMapIdentification(bool allowMapIdentification)369 void QgsRelationReferenceWidget::setAllowMapIdentification( bool allowMapIdentification )
370 {
371   mHighlightFeatureButton->setVisible( allowMapIdentification );
372   mMapIdentificationButton->setVisible( allowMapIdentification );
373   mAllowMapIdentification = allowMapIdentification;
374 }
375 
setOrderByValue(bool orderByValue)376 void QgsRelationReferenceWidget::setOrderByValue( bool orderByValue )
377 {
378   mOrderByValue = orderByValue;
379 }
380 
setFilterFields(const QStringList & filterFields)381 void QgsRelationReferenceWidget::setFilterFields( const QStringList &filterFields )
382 {
383   mFilterFields = filterFields;
384 }
385 
setOpenFormButtonVisible(bool openFormButtonVisible)386 void QgsRelationReferenceWidget::setOpenFormButtonVisible( bool openFormButtonVisible )
387 {
388   mOpenFormButton->setVisible( openFormButtonVisible );
389   mOpenFormButtonVisible = openFormButtonVisible;
390 }
391 
setChainFilters(bool chainFilters)392 void QgsRelationReferenceWidget::setChainFilters( bool chainFilters )
393 {
394   mChainFilters = chainFilters;
395 }
396 
setFilterExpression(const QString & expression)397 void QgsRelationReferenceWidget::setFilterExpression( const QString &expression )
398 {
399   mFilterExpression = expression;
400 }
401 
showEvent(QShowEvent * e)402 void QgsRelationReferenceWidget::showEvent( QShowEvent *e )
403 {
404   Q_UNUSED( e )
405 
406   mShown = true;
407   if ( !mInitialized )
408     init();
409 }
410 
init()411 void QgsRelationReferenceWidget::init()
412 {
413   if ( mReferencedLayer )
414   {
415     QApplication::setOverrideCursor( Qt::WaitCursor );
416 
417     QSet<QString> requestedAttrs;
418 
419     if ( !mFilterFields.isEmpty() )
420     {
421       for ( const QString &fieldName : std::as_const( mFilterFields ) )
422       {
423         int idx = mReferencedLayer->fields().lookupField( fieldName );
424 
425         if ( idx == -1 )
426           continue;
427 
428         QComboBox *cb = new QComboBox();
429         cb->setProperty( "Field", fieldName );
430         cb->setProperty( "FieldAlias", mReferencedLayer->attributeDisplayName( idx ) );
431         mFilterComboBoxes << cb;
432         QVariantList uniqueValues = qgis::setToList( mReferencedLayer->uniqueValues( idx ) );
433         cb->addItem( mReferencedLayer->attributeDisplayName( idx ) );
434         QVariant nullValue = QgsApplication::nullRepresentation();
435         cb->addItem( nullValue.toString(), QVariant( mReferencedLayer->fields().at( idx ).type() ) );
436 
437         std::sort( uniqueValues.begin(), uniqueValues.end(), qgsVariantLessThan );
438         const auto constUniqueValues = uniqueValues;
439         for ( const QVariant &v : constUniqueValues )
440         {
441           cb->addItem( v.toString(), v );
442         }
443 
444         connect( cb, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsRelationReferenceWidget::filterChanged );
445 
446         // Request this attribute for caching
447         requestedAttrs << fieldName;
448 
449         mFilterLayout->addWidget( cb );
450       }
451 
452       if ( mChainFilters )
453       {
454         QVariant nullValue = QgsApplication::nullRepresentation();
455 
456         QgsFeature ft;
457         QgsFeatureIterator fit = mFilterExpression.isEmpty()
458                                  ? mReferencedLayer->getFeatures()
459                                  : mReferencedLayer->getFeatures( mFilterExpression );
460         while ( fit.nextFeature( ft ) )
461         {
462           const int count = std::min( mFilterComboBoxes.count(), mFilterFields.count() );
463           for ( int i = 0; i < count - 1; i++ )
464           {
465             QVariant cv = ft.attribute( mFilterFields.at( i ) );
466             QVariant nv = ft.attribute( mFilterFields.at( i + 1 ) );
467             QString cf = cv.isNull() ? nullValue.toString() : cv.toString();
468             QString nf = nv.isNull() ? nullValue.toString() : nv.toString();
469             mFilterCache[mFilterFields[i]][cf] << nf;
470           }
471         }
472 
473         if ( !mFilterComboBoxes.isEmpty() )
474         {
475           QComboBox *cb = mFilterComboBoxes.first();
476           cb->setCurrentIndex( 0 );
477           disableChainedComboBoxes( cb );
478         }
479       }
480     }
481     else
482     {
483       mFilterContainer->hide();
484     }
485 
486     mComboBox->setSourceLayer( mReferencedLayer );
487     mComboBox->setDisplayExpression( mReferencedLayer->displayExpression() );
488     mComboBox->setAllowNull( mAllowNull );
489     mComboBox->setIdentifierFields( mReferencedFields );
490 
491     if ( ! mFilterExpression.isEmpty() )
492       mComboBox->setFilterExpression( mFilterExpression );
493 
494     QVariant nullValue = QgsApplication::nullRepresentation();
495 
496     if ( mChainFilters && mFeature.isValid() )
497     {
498       for ( int i = 0; i < mFilterFields.size(); i++ )
499       {
500         QVariant v = mFeature.attribute( mFilterFields[i] );
501         QString f = v.isNull() ? nullValue.toString() : v.toString();
502         mFilterComboBoxes.at( i )->setCurrentIndex( mFilterComboBoxes.at( i )->findText( f ) );
503       }
504     }
505 
506     // Only connect after iterating, to have only one iterator on the referenced table at once
507     connect( mComboBox, &QgsFeatureListComboBox::currentFeatureChanged, this, &QgsRelationReferenceWidget::comboReferenceChanged );
508 
509     QApplication::restoreOverrideCursor();
510 
511     mInitialized = true;
512   }
513 }
514 
highlightActionTriggered(QAction * action)515 void QgsRelationReferenceWidget::highlightActionTriggered( QAction *action )
516 {
517   if ( action == mHighlightFeatureAction )
518   {
519     highlightFeature();
520   }
521   else if ( action == mScaleHighlightFeatureAction )
522   {
523     highlightFeature( QgsFeature(), Scale );
524   }
525   else if ( action == mPanHighlightFeatureAction )
526   {
527     highlightFeature( QgsFeature(), Pan );
528   }
529 }
530 
openForm()531 void QgsRelationReferenceWidget::openForm()
532 {
533   QgsFeature feat = referencedFeature();
534 
535   if ( !feat.isValid() )
536     return;
537 
538   QgsAttributeEditorContext context( mEditorContext, mRelation, QgsAttributeEditorContext::Single, QgsAttributeEditorContext::StandaloneDialog );
539   QgsAttributeDialog attributeDialog( mReferencedLayer, new QgsFeature( feat ), true, this, true, context );
540   attributeDialog.exec();
541 }
542 
highlightFeature(QgsFeature f,CanvasExtent canvasExtent)543 void QgsRelationReferenceWidget::highlightFeature( QgsFeature f, CanvasExtent canvasExtent )
544 {
545   if ( !mCanvas )
546     return;
547 
548   if ( !f.isValid() )
549   {
550     f = referencedFeature();
551     if ( !f.isValid() )
552       return;
553   }
554 
555   if ( !f.hasGeometry() )
556   {
557     return;
558   }
559 
560   QgsGeometry geom = f.geometry();
561 
562   // scale or pan
563   if ( canvasExtent == Scale )
564   {
565     QgsRectangle featBBox = geom.boundingBox();
566     featBBox = mCanvas->mapSettings().layerToMapCoordinates( mReferencedLayer, featBBox );
567     QgsRectangle extent = mCanvas->extent();
568     if ( !extent.contains( featBBox ) )
569     {
570       extent.combineExtentWith( featBBox );
571       extent.scale( 1.1 );
572       mCanvas->setExtent( extent, true );
573       mCanvas->refresh();
574     }
575   }
576   else if ( canvasExtent == Pan )
577   {
578     QgsGeometry centroid = geom.centroid();
579     QgsPointXY center = centroid.asPoint();
580     center = mCanvas->mapSettings().layerToMapCoordinates( mReferencedLayer, center );
581     mCanvas->zoomByFactor( 1.0, &center ); // refresh is done in this method
582   }
583 
584   // highlight
585   deleteHighlight();
586   mHighlight = new QgsHighlight( mCanvas, f, mReferencedLayer );
587   QgsIdentifyMenu::styleHighlight( mHighlight );
588   mHighlight->show();
589 
590   QTimer *timer = new QTimer( this );
591   timer->setSingleShot( true );
592   connect( timer, &QTimer::timeout, this, &QgsRelationReferenceWidget::deleteHighlight );
593   timer->start( 3000 );
594 }
595 
deleteHighlight()596 void QgsRelationReferenceWidget::deleteHighlight()
597 {
598   if ( mHighlight )
599   {
600     mHighlight->hide();
601     delete mHighlight;
602   }
603   mHighlight = nullptr;
604 }
605 
mapIdentification()606 void QgsRelationReferenceWidget::mapIdentification()
607 {
608   if ( !mAllowMapIdentification || !mReferencedLayer )
609     return;
610 
611   const QgsVectorLayerTools *tools = mEditorContext.vectorLayerTools();
612   if ( !tools )
613     return;
614   if ( !mCanvas )
615     return;
616 
617   mMapToolIdentify->setLayer( mReferencedLayer );
618   setMapTool( mMapToolIdentify );
619 
620   connect( mMapToolIdentify, qOverload<const QgsFeature &>( &QgsMapToolIdentifyFeature::featureIdentified ), this, &QgsRelationReferenceWidget::featureIdentified );
621 
622   if ( mMessageBar )
623   {
624     QString title = tr( "Relation %1 for %2." ).arg( mRelation.name(), mReferencingLayer->name() );
625     QString msg = tr( "Identify a feature of %1 to be associated. Press &lt;ESC&gt; to cancel." ).arg( mReferencedLayer->name() );
626     mMessageBarItem = QgsMessageBar::createMessage( title, msg, this );
627     mMessageBar->pushItem( mMessageBarItem );
628   }
629 }
630 
comboReferenceChanged()631 void QgsRelationReferenceWidget::comboReferenceChanged()
632 {
633   mReferencedLayer->getFeatures( mComboBox->currentFeatureRequest() ).nextFeature( mFeature );
634   highlightFeature( mFeature );
635   updateAttributeEditorFrame( mFeature );
636 
637   emitForeignKeysChanged( mComboBox->identifierValues() );
638 }
639 
updateAttributeEditorFrame(const QgsFeature & feature)640 void QgsRelationReferenceWidget::updateAttributeEditorFrame( const QgsFeature &feature )
641 {
642   mOpenFormButton->setEnabled( feature.isValid() );
643   // Check if we're running with an embedded frame we need to update
644   if ( mAttributeEditorFrame && mReferencedAttributeForm )
645   {
646     mReferencedAttributeForm->setFeature( feature );
647   }
648 }
649 
allowAddFeatures() const650 bool QgsRelationReferenceWidget::allowAddFeatures() const
651 {
652   return mAllowAddFeatures;
653 }
654 
setAllowAddFeatures(bool allowAddFeatures)655 void QgsRelationReferenceWidget::setAllowAddFeatures( bool allowAddFeatures )
656 {
657   mAllowAddFeatures = allowAddFeatures;
658   updateAddEntryButton();
659 }
660 
relation() const661 QgsRelation QgsRelationReferenceWidget::relation() const
662 {
663   return mRelation;
664 }
665 
featureIdentified(const QgsFeature & feature)666 void QgsRelationReferenceWidget::featureIdentified( const QgsFeature &feature )
667 {
668   mComboBox->setCurrentFeature( feature );
669   mFeature = feature;
670 
671   mRemoveFKButton->setEnabled( mIsEditable );
672   highlightFeature( feature );
673   updateAttributeEditorFrame( feature );
674   emitForeignKeysChanged( foreignKeys(), true );
675 
676   unsetMapTool();
677 }
678 
setMapTool(QgsMapTool * mapTool)679 void QgsRelationReferenceWidget::setMapTool( QgsMapTool *mapTool )
680 {
681   mCurrentMapTool = mapTool;
682   mCanvas->setMapTool( mapTool );
683 
684   mWindowWidget = window();
685 
686   mCanvas->window()->raise();
687   mCanvas->activateWindow();
688   mCanvas->setFocus();
689   connect( mapTool, &QgsMapTool::deactivated, this, &QgsRelationReferenceWidget::mapToolDeactivated );
690 }
691 
unsetMapTool()692 void QgsRelationReferenceWidget::unsetMapTool()
693 {
694   // deactivate map tools if activated
695   if ( mCurrentMapTool )
696   {
697     /* this will call mapToolDeactivated */
698     mCanvas->unsetMapTool( mCurrentMapTool );
699 
700     if ( mCurrentMapTool == mMapToolDigitize )
701     {
702       disconnect( mCanvas, &QgsMapCanvas::keyPressed, this, &QgsRelationReferenceWidget::onKeyPressed );
703       disconnect( mMapToolDigitize, &QgsMapToolDigitizeFeature::digitizingCompleted, this, &QgsRelationReferenceWidget::entryAdded );
704     }
705     else
706     {
707       disconnect( mMapToolIdentify, qOverload<const QgsFeature &>( &QgsMapToolIdentifyFeature::featureIdentified ), this, &QgsRelationReferenceWidget::featureIdentified );
708     }
709   }
710 }
711 
onKeyPressed(QKeyEvent * e)712 void QgsRelationReferenceWidget::onKeyPressed( QKeyEvent *e )
713 {
714   if ( e->key() == Qt::Key_Escape )
715   {
716     unsetMapTool();
717   }
718 }
719 
mapToolDeactivated()720 void QgsRelationReferenceWidget::mapToolDeactivated()
721 {
722   if ( mWindowWidget )
723   {
724     mWindowWidget->raise();
725     mWindowWidget->activateWindow();
726   }
727 
728   if ( mMessageBar && mMessageBarItem )
729   {
730     mMessageBar->popWidget( mMessageBarItem );
731   }
732   mMessageBarItem = nullptr;
733 }
734 
filterChanged()735 void QgsRelationReferenceWidget::filterChanged()
736 {
737   QVariant nullValue = QgsApplication::nullRepresentation();
738 
739   QMap<QString, QString> filters;
740   QgsAttributeList attrs;
741 
742   QComboBox *scb = qobject_cast<QComboBox *>( sender() );
743 
744   Q_ASSERT( scb );
745 
746   QgsFeature f;
747   QgsFeatureIds featureIds;
748   QString filterExpression = mFilterExpression;
749 
750   // wrap the expression with parentheses as it might contain `OR`
751   if ( !filterExpression.isEmpty() )
752     filterExpression = QStringLiteral( " ( %1 ) " ).arg( filterExpression );
753 
754   // comboboxes have to be disabled before building filters
755   if ( mChainFilters )
756     disableChainedComboBoxes( scb );
757 
758   // build filters
759   const auto constMFilterComboBoxes = mFilterComboBoxes;
760   for ( QComboBox *cb : constMFilterComboBoxes )
761   {
762     if ( cb->currentIndex() != 0 )
763     {
764       const QString fieldName = cb->property( "Field" ).toString();
765 
766       if ( cb->currentText() == nullValue.toString() )
767       {
768         filters[fieldName] = QStringLiteral( "\"%1\" IS NULL" ).arg( fieldName );
769       }
770       else
771       {
772         filters[fieldName] = QgsExpression::createFieldEqualityExpression( fieldName, cb->currentText() );
773       }
774       attrs << mReferencedLayer->fields().lookupField( fieldName );
775     }
776   }
777 
778   if ( mChainFilters )
779   {
780     QComboBox *ccb = nullptr;
781     const auto constMFilterComboBoxes = mFilterComboBoxes;
782     for ( QComboBox *cb : constMFilterComboBoxes )
783     {
784       if ( !ccb )
785       {
786         if ( cb == scb )
787           ccb = cb;
788 
789         continue;
790       }
791 
792       if ( ccb->currentIndex() != 0 )
793       {
794         const QString fieldName = cb->property( "Field" ).toString();
795 
796         cb->blockSignals( true );
797         cb->clear();
798         cb->addItem( cb->property( "FieldAlias" ).toString() );
799 
800         // ccb = scb
801         // cb = scb + 1
802         QStringList texts;
803         const auto txts { mFilterCache[ccb->property( "Field" ).toString()][ccb->currentText()] };
804         for ( const QString &txt : txts )
805         {
806           QMap<QString, QString> filtersAttrs = filters;
807           filtersAttrs[fieldName] = QgsExpression::createFieldEqualityExpression( fieldName, txt );
808           QgsAttributeList subset = attrs;
809 
810           QString expression = filterExpression;
811           if ( ! filterExpression.isEmpty() && ! filtersAttrs.values().isEmpty() )
812             expression += QLatin1String( " AND " );
813 
814           expression += filtersAttrs.isEmpty() ? QString() : QStringLiteral( " ( " );
815           expression += filtersAttrs.values().join( QLatin1String( " AND " ) );
816           expression += filtersAttrs.isEmpty() ? QString() : QStringLiteral( " ) " );
817 
818           subset << mReferencedLayer->fields().lookupField( fieldName );
819 
820           QgsFeatureIterator it( mReferencedLayer->getFeatures( QgsFeatureRequest().setFilterExpression( expression ).setSubsetOfAttributes( subset ) ) );
821 
822           bool found = false;
823           while ( it.nextFeature( f ) )
824           {
825             if ( !featureIds.contains( f.id() ) )
826               featureIds << f.id();
827 
828             found = true;
829           }
830 
831           // item is only provided if at least 1 feature exists
832           if ( found )
833             texts << txt;
834         }
835 
836         texts.sort();
837         cb->addItems( texts );
838 
839         cb->setEnabled( true );
840         cb->blockSignals( false );
841 
842         ccb = cb;
843       }
844     }
845   }
846 
847   if ( ! filterExpression.isEmpty() && ! filters.values().isEmpty() )
848     filterExpression += QLatin1String( " AND " );
849 
850   filterExpression += filters.isEmpty() ? QString() : QStringLiteral( " ( " );
851   filterExpression += filters.values().join( QLatin1String( " AND " ) );
852   filterExpression += filters.isEmpty() ? QString() : QStringLiteral( " ) " );
853 
854   mComboBox->setFilterExpression( filterExpression );
855 }
856 
addEntry()857 void QgsRelationReferenceWidget::addEntry()
858 {
859   if ( !mReferencedLayer )
860     return;
861 
862   const QgsVectorLayerTools *tools = mEditorContext.vectorLayerTools();
863   if ( !tools )
864     return;
865   if ( !mCanvas )
866     return;
867 
868   // no geometry, skip the digitizing
869   if ( mReferencedLayer->geometryType() == QgsWkbTypes::UnknownGeometry || mReferencedLayer->geometryType() == QgsWkbTypes::NullGeometry )
870   {
871     QgsFeature f( mReferencedLayer->fields() );
872     entryAdded( f );
873     return;
874   }
875 
876   mMapToolDigitize->setLayer( mReferencedLayer );
877   setMapTool( mMapToolDigitize );
878 
879   connect( mMapToolDigitize, &QgsMapToolDigitizeFeature::digitizingCompleted, this, &QgsRelationReferenceWidget::entryAdded );
880   connect( mCanvas, &QgsMapCanvas::keyPressed, this, &QgsRelationReferenceWidget::onKeyPressed );
881 
882   if ( mMessageBar )
883   {
884     QString title = tr( "Relation %1 for %2." ).arg( mRelation.name(), mReferencingLayer->name() );
885 
886     QString displayString = QgsVectorLayerUtils::getFeatureDisplayString( mReferencingLayer, mFormFeature );
887     QString msg = tr( "Link feature to %1 \"%2\" : Digitize the geometry for the new feature on layer %3. Press &lt;ESC&gt; to cancel." )
888                   .arg( mReferencingLayer->name(), displayString, mReferencedLayer->name() );
889     mMessageBarItem = QgsMessageBar::createMessage( title, msg, this );
890     mMessageBar->pushItem( mMessageBarItem );
891   }
892 
893 }
894 
entryAdded(const QgsFeature & feat)895 void QgsRelationReferenceWidget::entryAdded( const QgsFeature &feat )
896 {
897   QgsFeature f( feat );
898   QgsAttributeMap attributes;
899 
900   // if custom text is in the combobox and the displayExpression is simply a field, use the current text for the new feature
901   if ( mComboBox->itemText( mComboBox->currentIndex() ) != mComboBox->currentText() )
902   {
903     int fieldIdx = mReferencedLayer->fields().lookupField( mReferencedLayer->displayExpression() );
904 
905     if ( fieldIdx != -1 )
906     {
907       attributes.insert( fieldIdx, mComboBox->currentText() );
908     }
909   }
910 
911   if ( mEditorContext.vectorLayerTools()->addFeature( mReferencedLayer, attributes, f.geometry(), &f ) )
912   {
913     QVariantList attrs;
914     for ( const QString &fieldName : std::as_const( mReferencedFields ) )
915       attrs << f.attribute( fieldName );
916 
917     setForeignKeys( attrs );
918 
919     mAddEntryButton->setEnabled( false );
920   }
921 
922   unsetMapTool();
923 }
924 
updateAddEntryButton()925 void QgsRelationReferenceWidget::updateAddEntryButton()
926 {
927   mAddEntryButton->setVisible( mAllowAddFeatures && mMapToolDigitize );
928   mAddEntryButton->setEnabled( mReferencedLayer && mReferencedLayer->isEditable() );
929 }
930 
disableChainedComboBoxes(const QComboBox * scb)931 void QgsRelationReferenceWidget::disableChainedComboBoxes( const QComboBox *scb )
932 {
933   QComboBox *ccb = nullptr;
934   const auto constMFilterComboBoxes = mFilterComboBoxes;
935   for ( QComboBox *cb : constMFilterComboBoxes )
936   {
937     if ( !ccb )
938     {
939       if ( cb == scb )
940       {
941         ccb = cb;
942       }
943 
944       continue;
945     }
946 
947     cb->setCurrentIndex( 0 );
948     if ( ccb->currentIndex() == 0 )
949     {
950       cb->setEnabled( false );
951     }
952 
953     ccb = cb;
954   }
955 }
956 
emitForeignKeysChanged(const QVariantList & foreignKeys,bool force)957 void QgsRelationReferenceWidget::emitForeignKeysChanged( const QVariantList &foreignKeys, bool force )
958 {
959   if ( foreignKeys == mForeignKeys && force == false && qVariantListIsNull( foreignKeys ) == qVariantListIsNull( mForeignKeys ) )
960     return;
961 
962   mForeignKeys = foreignKeys;
963   Q_NOWARN_DEPRECATED_PUSH
964   emit foreignKeyChanged( foreignKeys.at( 0 ) );
965   Q_NOWARN_DEPRECATED_POP
966   emit foreignKeysChanged( foreignKeys );
967 }
968 
referencedLayerName() const969 QString QgsRelationReferenceWidget::referencedLayerName() const
970 {
971   return mReferencedLayerName;
972 }
973 
setReferencedLayerName(const QString & relationLayerName)974 void QgsRelationReferenceWidget::setReferencedLayerName( const QString &relationLayerName )
975 {
976   mReferencedLayerName = relationLayerName;
977 }
978 
referencedLayerId() const979 QString QgsRelationReferenceWidget::referencedLayerId() const
980 {
981   return mReferencedLayerId;
982 }
983 
setReferencedLayerId(const QString & relationLayerId)984 void QgsRelationReferenceWidget::setReferencedLayerId( const QString &relationLayerId )
985 {
986   mReferencedLayerId = relationLayerId;
987 }
988 
referencedLayerProviderKey() const989 QString QgsRelationReferenceWidget::referencedLayerProviderKey() const
990 {
991   return mReferencedLayerProviderKey;
992 }
993 
setReferencedLayerProviderKey(const QString & relationProviderKey)994 void QgsRelationReferenceWidget::setReferencedLayerProviderKey( const QString &relationProviderKey )
995 {
996   mReferencedLayerProviderKey = relationProviderKey;
997 }
998 
referencedLayerDataSource() const999 QString QgsRelationReferenceWidget::referencedLayerDataSource() const
1000 {
1001   return mReferencedLayerDataSource;
1002 }
1003 
setReferencedLayerDataSource(const QString & relationDataSource)1004 void QgsRelationReferenceWidget::setReferencedLayerDataSource( const QString &relationDataSource )
1005 {
1006   const QgsPathResolver resolver { QgsProject::instance()->pathResolver() };
1007   mReferencedLayerDataSource = resolver.writePath( relationDataSource );
1008 }
1009 
setFormFeature(const QgsFeature & formFeature)1010 void QgsRelationReferenceWidget::setFormFeature( const QgsFeature &formFeature )
1011 {
1012   mFormFeature = formFeature;
1013 }
1014