QGIS API Documentation 3.29.0-Master (8c80f25a4f)
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 mLineEdit->setShowClearButton( true );
41#ifdef Q_OS_MACX
42 mLineEdit->setPlaceholderText( tr( "Type to locate (⌘K)" ) );
43#else
44 mLineEdit->setPlaceholderText( tr( "Type to locate (Ctrl+K)" ) );
45#endif
46
47 int placeholderMinWidth = mLineEdit->fontMetrics().boundingRect( mLineEdit->placeholderText() ).width();
48 int minWidth = std::max( 200, static_cast< int >( placeholderMinWidth * 1.8 ) );
49 resize( minWidth, 30 );
50 QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
51 sizePolicy.setHorizontalStretch( 0 );
52 sizePolicy.setVerticalStretch( 0 );
53 setSizePolicy( sizePolicy );
54 setMinimumSize( QSize( minWidth, 0 ) );
55
56 QHBoxLayout *layout = new QHBoxLayout();
57 layout->setContentsMargins( 0, 0, 0, 0 );
58 layout->addWidget( mLineEdit );
59 setLayout( layout );
60
61 setFocusProxy( mLineEdit );
62
63 // setup floating container widget
64 mResultsContainer = new QgsFloatingWidget( parent ? parent->window() : nullptr );
65 mResultsContainer->setAnchorWidget( mLineEdit );
68
69 QHBoxLayout *containerLayout = new QHBoxLayout();
70 containerLayout->setContentsMargins( 0, 0, 0, 0 );
71 containerLayout->addWidget( mResultsView );
72 mResultsContainer->setLayout( containerLayout );
73 mResultsContainer->hide();
74
75 mResultsView->setModel( mModelBridge->proxyModel() );
76 mResultsView->setUniformRowHeights( true );
77
79 mResultsView->setIconSize( QSize( iconSize, iconSize ) );
80 mResultsView->recalculateSize();
81 mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
82
83 connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup );
84 connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry );
85 connect( mResultsView, &QAbstractItemView::customContextMenuRequested, this, &QgsLocatorWidget::showContextMenu );
86
87 connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded );
88 connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} );
89 connect( mModelBridge, &QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
90
91 // have a tiny delay between typing text in line edit and showing the window
92 mPopupTimer.setInterval( 100 );
93 mPopupTimer.setSingleShot( true );
94 connect( &mPopupTimer, &QTimer::timeout, this, &QgsLocatorWidget::performSearch );
95 mFocusTimer.setInterval( 110 );
96 mFocusTimer.setSingleShot( true );
97 connect( &mFocusTimer, &QTimer::timeout, this, &QgsLocatorWidget::triggerSearchAndShowList );
98
99 mLineEdit->installEventFilter( this );
100 mResultsContainer->installEventFilter( this );
101 mResultsView->installEventFilter( this );
102 installEventFilter( this );
103 window()->installEventFilter( this );
104
105 mModelBridge->locator()->registerFilter( new QgsLocatorFilterFilter( this, this ) );
106
107 mMenu = new QMenu( this );
108 QAction *menuAction = mLineEdit->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) ), QLineEdit::LeadingPosition );
109 connect( menuAction, &QAction::triggered, this, [ = ]
110 {
111 mFocusTimer.stop();
112 mResultsContainer->hide();
113 mMenu->exec( QCursor::pos() );
114 } );
115 connect( mMenu, &QMenu::aboutToShow, this, &QgsLocatorWidget::configMenuAboutToShow );
116
117 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
119 this, [ = ]
120 {
121 mModelBridge->setTransformContext( QgsProject::instance()->transformContext() );
122 } );
123}
124
126{
127 return mModelBridge->locator();
128}
129
131{
132 if ( mMapCanvas == canvas )
133 return;
134
135 for ( const QMetaObject::Connection &conn : std::as_const( mCanvasConnections ) )
136 {
137 disconnect( conn );
138 }
139 mCanvasConnections.clear();
140
141 mMapCanvas = canvas;
142 if ( mMapCanvas )
143 {
144 mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );
145 mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );
146 mCanvasConnections
147 << connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, [ = ]() {mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );} )
148 << connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, [ = ]() {mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );} ) ;
149 }
150}
151
152void QgsLocatorWidget::search( const QString &string )
153{
154 window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
155 if ( string.isEmpty() )
156 {
157 mLineEdit->setFocus();
158 mLineEdit->selectAll();
159 }
160 else
161 {
162 scheduleDelayedPopup();
163 mLineEdit->setFocus();
164 mLineEdit->setText( string );
165 performSearch();
166 }
167}
168
170{
171 mModelBridge->invalidateResults();
172 mResultsContainer->hide();
173}
174
175void QgsLocatorWidget::scheduleDelayedPopup()
176{
177 mPopupTimer.start();
178}
179
180void QgsLocatorWidget::resultAdded()
181{
182 bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
183 if ( selectFirst )
184 {
185 int row = -1;
186 bool selectable = false;
187 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
188 {
189 row++;
190 selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
191 }
192 if ( selectable )
193 mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
194 }
195}
196
197void QgsLocatorWidget::showContextMenu( const QPoint &point )
198{
199 QModelIndex index = mResultsView->indexAt( point );
200 if ( !index.isValid() )
201 return;
202
203 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
204 QMenu *contextMenu = new QMenu( mResultsView );
205 for ( auto resultAction : actions )
206 {
207 QAction *menuAction = new QAction( resultAction.text, contextMenu );
208 if ( !resultAction.iconPath.isEmpty() )
209 menuAction->setIcon( QIcon( resultAction.iconPath ) );
210 connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
211 contextMenu->addAction( menuAction );
212 }
213 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
214}
215
216void QgsLocatorWidget::performSearch()
217{
218 mPopupTimer.stop();
219 mModelBridge->performSearch( mLineEdit->text() );
220 showList();
221}
222
223void QgsLocatorWidget::showList()
224{
225 mResultsContainer->show();
226 mResultsContainer->raise();
227}
228
229void QgsLocatorWidget::triggerSearchAndShowList()
230{
231 if ( mModelBridge->proxyModel()->rowCount() == 0 )
232 performSearch();
233 else
234 showList();
235}
236
237bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
238{
239 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
240 {
241 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
242 switch ( keyEvent->key() )
243 {
244 case Qt::Key_Up:
245 case Qt::Key_Down:
246 case Qt::Key_PageUp:
247 case Qt::Key_PageDown:
248 triggerSearchAndShowList();
249 mHasSelectedResult = true;
250 QgsApplication::sendEvent( mResultsView, event );
251 return true;
252 case Qt::Key_Home:
253 case Qt::Key_End:
254 if ( keyEvent->modifiers() & Qt::ControlModifier )
255 {
256 triggerSearchAndShowList();
257 mHasSelectedResult = true;
258 QgsApplication::sendEvent( mResultsView, event );
259 return true;
260 }
261 break;
262 case Qt::Key_Enter:
263 case Qt::Key_Return:
264 acceptCurrentEntry();
265 return true;
266 case Qt::Key_Escape:
267 mResultsContainer->hide();
268 return true;
269 case Qt::Key_Tab:
270 if ( !mLineEdit->performCompletion() )
271 {
272 mHasSelectedResult = true;
273 mResultsView->selectNextResult();
274 }
275 return true;
276 case Qt::Key_Backtab:
277 mHasSelectedResult = true;
278 mResultsView->selectPreviousResult();
279 return true;
280 default:
281 break;
282 }
283 }
284 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
285 {
286 mHasSelectedResult = true;
287 }
288 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
289 {
290 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
291 {
292 mFocusTimer.stop();
293 mResultsContainer->hide();
294 }
295 }
296 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
297 {
298 mFocusTimer.start();
299 }
300 else if ( obj == window() && event->type() == QEvent::Resize )
301 {
302 mResultsView->recalculateSize();
303 }
304 return QWidget::eventFilter( obj, event );
305}
306
307void QgsLocatorWidget::configMenuAboutToShow()
308{
309 mMenu->clear();
310 for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
311 {
312 if ( !filter->enabled() )
313 continue;
314
315 QAction *action = new QAction( filter->displayName(), mMenu );
316 connect( action, &QAction::triggered, this, [ = ]
317 {
318 QString currentText = mLineEdit->text();
319 if ( currentText.isEmpty() )
320 currentText = tr( "<type here>" );
321 else
322 {
323 QStringList parts = currentText.split( ' ' );
324 if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
325 {
326 parts.pop_front();
327 currentText = parts.join( ' ' );
328 }
329 }
330
331 mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
332 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
333 } );
334 mMenu->addAction( action );
335 }
336 mMenu->addSeparator();
337 QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
338 connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
339 mMenu->addAction( configAction );
340}
341
342
343void QgsLocatorWidget::acceptCurrentEntry()
344{
345 if ( mModelBridge->hasQueueRequested() )
346 {
347 return;
348 }
349 else
350 {
351 if ( !mResultsView->isVisible() )
352 return;
353
354 QModelIndex index = mResultsView->currentIndex();
355 if ( !index.isValid() )
356 return;
357
358 mResultsContainer->hide();
359 mLineEdit->clearFocus();
360 mModelBridge->triggerResult( index );
361 }
362}
363
365
366//
367// QgsLocatorResultsView
368//
369
370QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
371 : QTreeView( parent )
372{
373 setRootIsDecorated( false );
374 setUniformRowHeights( true );
375 header()->hide();
376 header()->setStretchLastSection( true );
377}
378
379void QgsLocatorResultsView::recalculateSize()
380{
381 QStyleOptionViewItem optView;
382#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
383 optView.init( this );
384#else
385 optView.initFrom( this );
386#endif
387
388 // try to show about 20 rows
389 int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
390
391 // try to take up a sensible portion of window width (about half)
392 int width = std::max( 300, window()->size().width() / 2 );
393 QSize newSize( width, rowSize + frameWidth() * 2 );
394 // resize the floating widget this is contained within
395 parentWidget()->resize( newSize );
396 QTreeView::resize( newSize );
397
398 header()->resizeSection( 0, width / 2 );
399 header()->resizeSection( 1, 0 );
400}
401
402void QgsLocatorResultsView::selectNextResult()
403{
404 const int rowCount = model()->rowCount( QModelIndex() );
405 if ( rowCount == 0 )
406 return;
407
408 int nextRow = currentIndex().row() + 1;
409 nextRow = nextRow % rowCount;
410 setCurrentIndex( model()->index( nextRow, 0 ) );
411}
412
413void QgsLocatorResultsView::selectPreviousResult()
414{
415 const int rowCount = model()->rowCount( QModelIndex() );
416 if ( rowCount == 0 )
417 return;
418
419 int previousRow = currentIndex().row() - 1;
420 if ( previousRow < 0 )
421 previousRow = rowCount - 1;
422 setCurrentIndex( model()->index( previousRow, 0 ) );
423}
424
425//
426// QgsLocatorFilterFilter
427//
428
429QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
430 : QgsLocatorFilter( parent )
431 , mLocator( locator )
432{}
433
434QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
435{
436 return new QgsLocatorFilterFilter( mLocator );
437}
438
439QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
440{
442}
443
444void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
445{
446 if ( !string.isEmpty() )
447 {
448 //only shows results when nothing typed
449 return;
450 }
451
452 for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
453 {
454 if ( feedback->isCanceled() )
455 return;
456
457 if ( filter == this || !filter || !filter->enabled() )
458 continue;
459
460 QgsLocatorResult result;
461 result.displayString = filter->activePrefix();
462 result.description = filter->displayName();
463 result.userData = QString( filter->activePrefix() + ' ' );
464 result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
465 emit resultFetched( result );
466 }
467}
468
469void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
470{
471 mLocator->search( result.userData.toString() );
472}
473
474QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
475 : QgsFilterLineEdit( parent )
476 , mLocatorWidget( locator )
477{
478 connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
479}
480
481void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
482{
483 // this adds the completion as grey text at the right of the cursor
484 // see https://stackoverflow.com/a/50425331/1548052
485 // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
486 // this sounds acceptable as it is not very likely to use completion for super long texts
487 // for more details see https://stackoverflow.com/a/54218192/1548052
488
489 QLineEdit::paintEvent( event );
490
491 if ( !hasFocus() )
492 return;
493
494 QString currentText = text();
495
496 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
497 return;
498
499 const QStringList completionList = mLocatorWidget->locator()->completionList();
500
501 mCompletionText.clear();
502 QString completion;
503 for ( const QString &candidate : completionList )
504 {
505 if ( candidate.startsWith( currentText ) )
506 {
507 completion = candidate.right( candidate.length() - currentText.length() );
508 mCompletionText = candidate;
509 break;
510 }
511 }
512
513 if ( completion.isEmpty() )
514 return;
515
516 ensurePolished(); // ensure font() is up to date
517
518 QRect cr = cursorRect();
519 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
520
521 QTextLayout l( completion, font() );
522 l.beginLayout();
523 QTextLine line = l.createLine();
524 line.setLineWidth( width() - pos.x() );
525 line.setPosition( pos );
526 l.endLayout();
527
528 QPainter p( this );
529 p.setPen( QPen( Qt::gray, 1 ) );
530 l.draw( &p, QPoint( 0, 0 ) );
531}
532
533bool QgsLocatorLineEdit::performCompletion()
534{
535 if ( !mCompletionText.isEmpty() )
536 {
537 setText( mCompletionText );
538 mCompletionText.clear();
539 return true;
540 }
541 else
542 return false;
543}
544
545
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:45
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
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...
@ 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.
@ 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.
@ ResultActionsRole
The actions to be shown for the given result in a context menu.
Encapsulates properties of an individual matching result found by a QgsLocatorFilter.
QVariant userData
Custom reference or other data set by the filter.
QString description
Descriptive text for result.
QString displayString
String displayed for result.
QIcon icon
Icon for result.
A special locator widget which allows searching for matching results from a QgsLocator and presenting...
void configTriggered()
Emitted when the configure option is triggered in the widget.
QgsLocatorWidget(QWidget *parent SIP_TRANSFERTHIS=nullptr)
Constructor for QgsLocatorWidget.
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:59
void searchPrepared()
Emitted when locator has prepared the search (.
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
Definition: qgslocator.cpp:93
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Definition: qgslocator.cpp:56
Map canvas is a class for displaying all GIS data types on a canvas.
Definition: qgsmapcanvas.h:90
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:476
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,...