QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgslocatorwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslocatorwidget.cpp
3 --------------------
4 begin : May 2017
5 copyright : (C) 2017 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgslocator.h"
19#include "qgslocatormodel.h"
20#include "qgslocatorwidget.h"
22#include "qgsfilterlineedit.h"
23#include "qgsmapcanvas.h"
24#include "qgsapplication.h"
25#include "qgslogger.h"
26#include "qgsguiutils.h"
27
28#include <QLayout>
29#include <QCompleter>
30#include <QMenu>
31#include <QTextLayout>
32#include <QTextLine>
33
35 : QWidget( parent )
36 , mModelBridge( new QgsLocatorModelBridge( this ) )
37 , mLineEdit( new QgsLocatorLineEdit( this ) )
38 , mResultsView( new QgsLocatorResultsView() )
39{
40 setObjectName( QStringLiteral( "LocatorWidget" ) );
41 mLineEdit->setShowClearButton( true );
42#ifdef Q_OS_MACX
43 mLineEdit->setPlaceholderText( tr( "Type to locate (⌘K)" ) );
44#else
45 mLineEdit->setPlaceholderText( tr( "Type to locate (Ctrl+K)" ) );
46#endif
47
48 int placeholderMinWidth = mLineEdit->fontMetrics().boundingRect( mLineEdit->placeholderText() ).width();
49 int minWidth = std::max( 200, static_cast< int >( placeholderMinWidth * 1.8 ) );
50 resize( minWidth, 30 );
51 QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
52 sizePolicy.setHorizontalStretch( 0 );
53 sizePolicy.setVerticalStretch( 0 );
54 setSizePolicy( sizePolicy );
55 setMinimumSize( QSize( minWidth, 0 ) );
56
57 QHBoxLayout *layout = new QHBoxLayout();
58 layout->setContentsMargins( 0, 0, 0, 0 );
59 layout->addWidget( mLineEdit );
60 setLayout( layout );
61
62 setFocusProxy( mLineEdit );
63
64 // setup floating container widget
65 mResultsContainer = new QgsFloatingWidget( parent ? parent->window() : nullptr );
66 mResultsContainer->setAnchorWidget( mLineEdit );
69
70 QHBoxLayout *containerLayout = new QHBoxLayout();
71 containerLayout->setContentsMargins( 0, 0, 0, 0 );
72 containerLayout->addWidget( mResultsView );
73 mResultsContainer->setLayout( containerLayout );
74 mResultsContainer->hide();
75
76 mResultsView->setModel( mModelBridge->proxyModel() );
77 mResultsView->setUniformRowHeights( true );
78
80 mResultsView->setIconSize( QSize( iconSize, iconSize ) );
81 mResultsView->recalculateSize();
82 mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
83
84 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup );
85 connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry );
86 connect( mResultsView, &QAbstractItemView::customContextMenuRequested, this, &QgsLocatorWidget::showContextMenu );
87
88 connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded );
89 connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} );
90 connect( mModelBridge, &QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
91
92 // have a tiny delay between typing text in line edit and showing the window
93 mPopupTimer.setInterval( 100 );
94 mPopupTimer.setSingleShot( true );
95 connect( &mPopupTimer, &QTimer::timeout, this, &QgsLocatorWidget::performSearch );
96 mFocusTimer.setInterval( 110 );
97 mFocusTimer.setSingleShot( true );
98 connect( &mFocusTimer, &QTimer::timeout, this, &QgsLocatorWidget::triggerSearchAndShowList );
99
100 mLineEdit->installEventFilter( this );
101 mResultsContainer->installEventFilter( this );
102 mResultsView->installEventFilter( this );
103 installEventFilter( this );
104 window()->installEventFilter( this );
105
106 mModelBridge->locator()->registerFilter( new QgsLocatorFilterFilter( this, this ) );
107
108 mMenu = new QMenu( this );
109 QAction *menuAction = mLineEdit->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) ), QLineEdit::LeadingPosition );
110 connect( menuAction, &QAction::triggered, this, [ = ]
111 {
112 mFocusTimer.stop();
113 mResultsContainer->hide();
114 mMenu->exec( QCursor::pos() );
115 } );
116 connect( mMenu, &QMenu::aboutToShow, this, &QgsLocatorWidget::configMenuAboutToShow );
117
118 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
120 this, [ = ]
121 {
122 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
123 } );
124}
125
127{
128 return mModelBridge->locator();
129}
130
132{
133 if ( mMapCanvas == canvas )
134 return;
135
136 for ( const QMetaObject::Connection &conn : std::as_const( mCanvasConnections ) )
137 {
138 disconnect( conn );
139 }
140 mCanvasConnections.clear();
141
142 mMapCanvas = canvas;
143 if ( mMapCanvas )
144 {
145 mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );
146 mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );
147 mCanvasConnections
148 << connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, [ = ]() {mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );} )
149 << connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, [ = ]() {mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );} ) ;
150 }
151}
152
153void QgsLocatorWidget::setPlaceholderText( const QString &text )
154{
155 mLineEdit->setPlaceholderText( text );
156}
157
159{
160 mResultsContainer->setAnchorPoint( anchorPoint );
161 mResultsContainer->setAnchorWidgetPoint( anchorWidgetPoint );
162}
163
164void QgsLocatorWidget::search( const QString &string )
165{
166 window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
167 if ( string.isEmpty() )
168 {
169 mLineEdit->setFocus();
170 mLineEdit->selectAll();
171 }
172 else
173 {
174 scheduleDelayedPopup();
175 mLineEdit->setFocus();
176 mLineEdit->setText( string );
177 performSearch();
178 }
179}
180
182{
183 mModelBridge->invalidateResults();
184 mResultsContainer->hide();
185}
186
187void QgsLocatorWidget::scheduleDelayedPopup()
188{
189 mPopupTimer.start();
190}
191
192void QgsLocatorWidget::resultAdded()
193{
194 bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
195 if ( selectFirst )
196 {
197 int row = -1;
198 bool selectable = false;
199 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
200 {
201 row++;
202 selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
203 }
204 if ( selectable )
205 mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
206 }
207}
208
209void QgsLocatorWidget::showContextMenu( const QPoint &point )
210{
211 QModelIndex index = mResultsView->indexAt( point );
212 if ( !index.isValid() )
213 return;
214
215 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, static_cast< int >( QgsLocatorModel::CustomRole::ResultActions ) ).value<QList<QgsLocatorResult::ResultAction>>();
216 QMenu *contextMenu = new QMenu( mResultsView );
217 for ( auto resultAction : actions )
218 {
219 QAction *menuAction = new QAction( resultAction.text, contextMenu );
220 if ( !resultAction.iconPath.isEmpty() )
221 menuAction->setIcon( QIcon( resultAction.iconPath ) );
222 connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
223 contextMenu->addAction( menuAction );
224 }
225 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
226}
227
228void QgsLocatorWidget::performSearch()
229{
230 mPopupTimer.stop();
231 mModelBridge->performSearch( mLineEdit->text() );
232 showList();
233}
234
235void QgsLocatorWidget::showList()
236{
237 mResultsContainer->show();
238 mResultsContainer->raise();
239}
240
241void QgsLocatorWidget::triggerSearchAndShowList()
242{
243 if ( mModelBridge->proxyModel()->rowCount() == 0 )
244 performSearch();
245 else
246 showList();
247}
248
249bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
250{
251 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
252 {
253 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
254 switch ( keyEvent->key() )
255 {
256 case Qt::Key_Up:
257 case Qt::Key_Down:
258 case Qt::Key_PageUp:
259 case Qt::Key_PageDown:
260 triggerSearchAndShowList();
261 mHasSelectedResult = true;
262 QgsApplication::sendEvent( mResultsView, event );
263 return true;
264 case Qt::Key_Home:
265 case Qt::Key_End:
266 if ( keyEvent->modifiers() & Qt::ControlModifier )
267 {
268 triggerSearchAndShowList();
269 mHasSelectedResult = true;
270 QgsApplication::sendEvent( mResultsView, event );
271 return true;
272 }
273 break;
274 case Qt::Key_Enter:
275 case Qt::Key_Return:
276 acceptCurrentEntry();
277 return true;
278 case Qt::Key_Escape:
279 mResultsContainer->hide();
280 return true;
281 case Qt::Key_Tab:
282 if ( !mLineEdit->performCompletion() )
283 {
284 mHasSelectedResult = true;
285 mResultsView->selectNextResult();
286 }
287 return true;
288 case Qt::Key_Backtab:
289 mHasSelectedResult = true;
290 mResultsView->selectPreviousResult();
291 return true;
292 default:
293 break;
294 }
295 }
296 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
297 {
298 mHasSelectedResult = true;
299 }
300 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
301 {
302 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
303 {
304 mFocusTimer.stop();
305 mResultsContainer->hide();
306 }
307 }
308 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
309 {
310 mFocusTimer.start();
311 }
312 else if ( obj == window() && event->type() == QEvent::Resize )
313 {
314 mResultsView->recalculateSize();
315 }
316 return QWidget::eventFilter( obj, event );
317}
318
319void QgsLocatorWidget::configMenuAboutToShow()
320{
321 mMenu->clear();
322 for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
323 {
324 if ( !filter->enabled() )
325 continue;
326
327 QAction *action = new QAction( filter->displayName(), mMenu );
328 connect( action, &QAction::triggered, this, [ = ]
329 {
330 QString currentText = mLineEdit->text();
331 if ( currentText.isEmpty() )
332 currentText = tr( "<type here>" );
333 else
334 {
335 QStringList parts = currentText.split( ' ' );
336 if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
337 {
338 parts.pop_front();
339 currentText = parts.join( ' ' );
340 }
341 }
342
343 mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
344 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
345 } );
346 mMenu->addAction( action );
347 }
348 mMenu->addSeparator();
349 QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
350 connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
351 mMenu->addAction( configAction );
352}
353
354
355void QgsLocatorWidget::acceptCurrentEntry()
356{
357 if ( mModelBridge->hasQueueRequested() )
358 {
359 return;
360 }
361 else
362 {
363 if ( !mResultsView->isVisible() )
364 return;
365
366 QModelIndex index = mResultsView->currentIndex();
367 if ( !index.isValid() )
368 return;
369
370 mResultsContainer->hide();
371 mLineEdit->clearFocus();
372 mModelBridge->triggerResult( index );
373 }
374}
375
377
378//
379// QgsLocatorResultsView
380//
381
382QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
383 : QTreeView( parent )
384{
385 setRootIsDecorated( false );
386 setUniformRowHeights( true );
387 header()->hide();
388 header()->setStretchLastSection( true );
389}
390
391void QgsLocatorResultsView::recalculateSize()
392{
393 QStyleOptionViewItem optView;
394#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
395 optView.init( this );
396#else
397 optView.initFrom( this );
398#endif
399
400 // try to show about 20 rows
401 int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
402
403 // try to take up a sensible portion of window width (about half)
404 int width = std::max( 300, window()->size().width() / 2 );
405 QSize newSize( width, rowSize + frameWidth() * 2 );
406 // resize the floating widget this is contained within
407 parentWidget()->resize( newSize );
408 QTreeView::resize( newSize );
409
410 header()->resizeSection( 0, width / 2 );
411 header()->resizeSection( 1, 0 );
412}
413
414void QgsLocatorResultsView::selectNextResult()
415{
416 const int rowCount = model()->rowCount( QModelIndex() );
417 if ( rowCount == 0 )
418 return;
419
420 int nextRow = currentIndex().row() + 1;
421 nextRow = nextRow % rowCount;
422 setCurrentIndex( model()->index( nextRow, 0 ) );
423}
424
425void QgsLocatorResultsView::selectPreviousResult()
426{
427 const int rowCount = model()->rowCount( QModelIndex() );
428 if ( rowCount == 0 )
429 return;
430
431 int previousRow = currentIndex().row() - 1;
432 if ( previousRow < 0 )
433 previousRow = rowCount - 1;
434 setCurrentIndex( model()->index( previousRow, 0 ) );
435}
436
437//
438// QgsLocatorFilterFilter
439//
440
441QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
442 : QgsLocatorFilter( parent )
443 , mLocator( locator )
444{}
445
446QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
447{
448 return new QgsLocatorFilterFilter( mLocator );
449}
450
451QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
452{
454}
455
456void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
457{
458 if ( !string.isEmpty() )
459 {
460 //only shows results when nothing typed
461 return;
462 }
463
464 for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
465 {
466 if ( feedback->isCanceled() )
467 return;
468
469 if ( filter == this || !filter || !filter->enabled() )
470 continue;
471
472 QgsLocatorResult result;
473 result.displayString = filter->activePrefix();
474 result.description = filter->displayName();
475 result.setUserData( QString( filter->activePrefix() + ' ' ) );
476 result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
477 emit resultFetched( result );
478 }
479}
480
481void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
482{
483 mLocator->search( result.userData().toString() );
484}
485
486QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
487 : QgsFilterLineEdit( parent )
488 , mLocatorWidget( locator )
489{
490 connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
491}
492
493void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
494{
495 // this adds the completion as grey text at the right of the cursor
496 // see https://stackoverflow.com/a/50425331/1548052
497 // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
498 // this sounds acceptable as it is not very likely to use completion for super long texts
499 // for more details see https://stackoverflow.com/a/54218192/1548052
500
501 QLineEdit::paintEvent( event );
502
503 if ( !hasFocus() )
504 return;
505
506 QString currentText = text();
507
508 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
509 return;
510
511 const QStringList completionList = mLocatorWidget->locator()->completionList();
512
513 mCompletionText.clear();
514 QString completion;
515 for ( const QString &candidate : completionList )
516 {
517 if ( candidate.startsWith( currentText ) )
518 {
519 completion = candidate.right( candidate.length() - currentText.length() );
520 mCompletionText = candidate;
521 break;
522 }
523 }
524
525 if ( completion.isEmpty() )
526 return;
527
528 ensurePolished(); // ensure font() is up to date
529
530 QRect cr = cursorRect();
531 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
532
533 QTextLayout l( completion, font() );
534 l.beginLayout();
535 QTextLine line = l.createLine();
536 line.setLineWidth( width() - pos.x() );
537 line.setPosition( pos );
538 l.endLayout();
539
540 QPainter p( this );
541 p.setPen( QPen( Qt::gray, 1 ) );
542 l.draw( &p, QPoint( 0, 0 ) );
543}
544
545bool QgsLocatorLineEdit::performCompletion()
546{
547 if ( !mCompletionText.isEmpty() )
548 {
549 setText( mCompletionText );
550 mCompletionText.clear();
551 return true;
552 }
553 else
554 return false;
555}
556
557
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:53
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
A QWidget subclass for creating widgets which float outside of the normal Qt layout system.
void setAnchorWidget(QWidget *widget)
Sets the widget to "anchor" the floating widget to.
void setAnchorWidgetPoint(AnchorPoint point)
Returns the anchor widget's anchor point, which corresponds to the point on the anchor widget which t...
AnchorPoint
Reference points for anchoring widget position.
@ BottomLeft
Bottom-left of widget.
@ TopLeft
Top-left of widget.
void setAnchorPoint(AnchorPoint point)
Sets the floating widget's anchor point, which corresponds to the point on the widget which should re...
Encapsulates the properties relating to the context of a locator search.
Abstract base class for filters which collect locator results.
QFlags< Flag > Flags
@ FlagFast
Filter finds results quickly and can be safely run in the main thread.
The QgsLocatorModelBridge class provides the core functionality to be used in a locator widget.
Q_INVOKABLE QgsLocatorProxyModel * proxyModel() const
Returns the proxy model.
void isRunningChanged()
Emitted when the running status changes.
void resultAdded()
Emitted when a result is added.
void triggerResult(const QModelIndex &index, const int actionId=-1)
Triggers the result at given index and with optional actionId if an additional action was triggered.
void setTransformContext(const QgsCoordinateTransformContext &context)
Sets the coordinate transform context, which should be used whenever the locator constructs a coordin...
QgsLocator * locator() const
Returns the locator.
bool hasQueueRequested() const
Returns true if some text to be search is pending in the queue.
Q_INVOKABLE void performSearch(const QString &text)
Perform a search.
void resultsCleared()
Emitted when the results are cleared.
void updateCanvasCrs(const QgsCoordinateReferenceSystem &crs)
Update the canvas CRS used to create search context.
void updateCanvasExtent(const QgsRectangle &extent)
Update the canvas extent used to create search context.
void invalidateResults()
This will invalidate current search results.
@ ResultActions
The actions to be shown for the given result in a context menu.
Encapsulates properties of an individual matching result found by a QgsLocatorFilter.
QString description
Descriptive text for result.
void setUserData(const QVariant &userData)
Set userData for the locator result.
QString displayString
String displayed for result.
QVariant userData() const
Returns the userData.
QIcon icon
Icon for result.
A special locator widget which allows searching for matching results from a QgsLocator and presenting...
void setResultContainerAnchors(QgsFloatingWidget::AnchorPoint anchorPoint, QgsFloatingWidget::AnchorPoint anchorWidgetPoint)
Sets the result container anchorPoint and anchorWidgetPoint position.
void configTriggered()
Emitted when the configure option is triggered in the widget.
QgsLocatorWidget(QWidget *parent SIP_TRANSFERTHIS=nullptr)
Constructor for QgsLocatorWidget.
void setPlaceholderText(const QString &text)
Set placeholder text for the line edit.
void setMapCanvas(QgsMapCanvas *canvas)
Sets a map canvas to associate with the widget.
void search(const QString &string)
Triggers the locator widget to focus, open and start searching for a specified string.
bool eventFilter(QObject *obj, QEvent *event) override
void invalidateResults()
Invalidates the current search results, e.g.
QgsLocator * locator()
Returns a pointer to the locator utilized by this widget.
Handles the management of QgsLocatorFilter objects and async collection of search results from them.
Definition: qgslocator.h:61
void searchPrepared()
Emitted when locator has prepared the search (.
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
Definition: qgslocator.cpp:96
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Definition: qgslocator.cpp:63
Map canvas is a class for displaying all GIS data types on a canvas.
Definition: qgsmapcanvas.h:93
void extentsChanged()
Emitted when the extents of the map change.
void destinationCrsChanged()
Emitted when map CRS has changed.
const QgsMapSettings & mapSettings() const
Gets access to properties used for map rendering.
QgsRectangle visibleExtent() const
Returns the actual extent derived from requested extent that takes output image size into account.
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system for the map render.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:481
void transformContextChanged()
Emitted when the project transformContext() is changed.
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window's toolbar icons.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...