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, ¢er ); // 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 <ESC> 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 <ESC> 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