QGIS API Documentation 3.34.0-Prizren (ffbdd678812)
Loading...
Searching...
No Matches
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
79 int iconSize = QgsGuiUtils::scaleIconSize( 16 );
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::search( const QString &string )
154{
155 window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
156 if ( string.isEmpty() )
157 {
158 mLineEdit->setFocus();
159 mLineEdit->selectAll();
160 }
161 else
162 {
163 scheduleDelayedPopup();
164 mLineEdit->setFocus();
165 mLineEdit->setText( string );
166 performSearch();
167 }
168}
169
171{
172 mModelBridge->invalidateResults();
173 mResultsContainer->hide();
174}
175
176void QgsLocatorWidget::scheduleDelayedPopup()
177{
178 mPopupTimer.start();
179}
180
181void QgsLocatorWidget::resultAdded()
182{
183 bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
184 if ( selectFirst )
185 {
186 int row = -1;
187 bool selectable = false;
188 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
189 {
190 row++;
191 selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
192 }
193 if ( selectable )
194 mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
195 }
196}
197
198void QgsLocatorWidget::showContextMenu( const QPoint &point )
199{
200 QModelIndex index = mResultsView->indexAt( point );
201 if ( !index.isValid() )
202 return;
203
204 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
205 QMenu *contextMenu = new QMenu( mResultsView );
206 for ( auto resultAction : actions )
207 {
208 QAction *menuAction = new QAction( resultAction.text, contextMenu );
209 if ( !resultAction.iconPath.isEmpty() )
210 menuAction->setIcon( QIcon( resultAction.iconPath ) );
211 connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
212 contextMenu->addAction( menuAction );
213 }
214 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
215}
216
217void QgsLocatorWidget::performSearch()
218{
219 mPopupTimer.stop();
220 mModelBridge->performSearch( mLineEdit->text() );
221 showList();
222}
223
224void QgsLocatorWidget::showList()
225{
226 mResultsContainer->show();
227 mResultsContainer->raise();
228}
229
230void QgsLocatorWidget::triggerSearchAndShowList()
231{
232 if ( mModelBridge->proxyModel()->rowCount() == 0 )
233 performSearch();
234 else
235 showList();
236}
237
238bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
239{
240 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
241 {
242 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
243 switch ( keyEvent->key() )
244 {
245 case Qt::Key_Up:
246 case Qt::Key_Down:
247 case Qt::Key_PageUp:
248 case Qt::Key_PageDown:
249 triggerSearchAndShowList();
250 mHasSelectedResult = true;
251 QgsApplication::sendEvent( mResultsView, event );
252 return true;
253 case Qt::Key_Home:
254 case Qt::Key_End:
255 if ( keyEvent->modifiers() & Qt::ControlModifier )
256 {
257 triggerSearchAndShowList();
258 mHasSelectedResult = true;
259 QgsApplication::sendEvent( mResultsView, event );
260 return true;
261 }
262 break;
263 case Qt::Key_Enter:
264 case Qt::Key_Return:
265 acceptCurrentEntry();
266 return true;
267 case Qt::Key_Escape:
268 mResultsContainer->hide();
269 return true;
270 case Qt::Key_Tab:
271 if ( !mLineEdit->performCompletion() )
272 {
273 mHasSelectedResult = true;
274 mResultsView->selectNextResult();
275 }
276 return true;
277 case Qt::Key_Backtab:
278 mHasSelectedResult = true;
279 mResultsView->selectPreviousResult();
280 return true;
281 default:
282 break;
283 }
284 }
285 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
286 {
287 mHasSelectedResult = true;
288 }
289 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
290 {
291 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
292 {
293 mFocusTimer.stop();
294 mResultsContainer->hide();
295 }
296 }
297 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
298 {
299 mFocusTimer.start();
300 }
301 else if ( obj == window() && event->type() == QEvent::Resize )
302 {
303 mResultsView->recalculateSize();
304 }
305 return QWidget::eventFilter( obj, event );
306}
307
308void QgsLocatorWidget::configMenuAboutToShow()
309{
310 mMenu->clear();
311 for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
312 {
313 if ( !filter->enabled() )
314 continue;
315
316 QAction *action = new QAction( filter->displayName(), mMenu );
317 connect( action, &QAction::triggered, this, [ = ]
318 {
319 QString currentText = mLineEdit->text();
320 if ( currentText.isEmpty() )
321 currentText = tr( "<type here>" );
322 else
323 {
324 QStringList parts = currentText.split( ' ' );
325 if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
326 {
327 parts.pop_front();
328 currentText = parts.join( ' ' );
329 }
330 }
331
332 mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
333 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
334 } );
335 mMenu->addAction( action );
336 }
337 mMenu->addSeparator();
338 QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
339 connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
340 mMenu->addAction( configAction );
341}
342
343
344void QgsLocatorWidget::acceptCurrentEntry()
345{
346 if ( mModelBridge->hasQueueRequested() )
347 {
348 return;
349 }
350 else
351 {
352 if ( !mResultsView->isVisible() )
353 return;
354
355 QModelIndex index = mResultsView->currentIndex();
356 if ( !index.isValid() )
357 return;
358
359 mResultsContainer->hide();
360 mLineEdit->clearFocus();
361 mModelBridge->triggerResult( index );
362 }
363}
364
366
367//
368// QgsLocatorResultsView
369//
370
371QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
372 : QTreeView( parent )
373{
374 setRootIsDecorated( false );
375 setUniformRowHeights( true );
376 header()->hide();
377 header()->setStretchLastSection( true );
378}
379
380void QgsLocatorResultsView::recalculateSize()
381{
382 QStyleOptionViewItem optView;
383#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
384 optView.init( this );
385#else
386 optView.initFrom( this );
387#endif
388
389 // try to show about 20 rows
390 int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
391
392 // try to take up a sensible portion of window width (about half)
393 int width = std::max( 300, window()->size().width() / 2 );
394 QSize newSize( width, rowSize + frameWidth() * 2 );
395 // resize the floating widget this is contained within
396 parentWidget()->resize( newSize );
397 QTreeView::resize( newSize );
398
399 header()->resizeSection( 0, width / 2 );
400 header()->resizeSection( 1, 0 );
401}
402
403void QgsLocatorResultsView::selectNextResult()
404{
405 const int rowCount = model()->rowCount( QModelIndex() );
406 if ( rowCount == 0 )
407 return;
408
409 int nextRow = currentIndex().row() + 1;
410 nextRow = nextRow % rowCount;
411 setCurrentIndex( model()->index( nextRow, 0 ) );
412}
413
414void QgsLocatorResultsView::selectPreviousResult()
415{
416 const int rowCount = model()->rowCount( QModelIndex() );
417 if ( rowCount == 0 )
418 return;
419
420 int previousRow = currentIndex().row() - 1;
421 if ( previousRow < 0 )
422 previousRow = rowCount - 1;
423 setCurrentIndex( model()->index( previousRow, 0 ) );
424}
425
426//
427// QgsLocatorFilterFilter
428//
429
430QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
431 : QgsLocatorFilter( parent )
432 , mLocator( locator )
433{}
434
435QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
436{
437 return new QgsLocatorFilterFilter( mLocator );
438}
439
440QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
441{
443}
444
445void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
446{
447 if ( !string.isEmpty() )
448 {
449 //only shows results when nothing typed
450 return;
451 }
452
453 for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
454 {
455 if ( feedback->isCanceled() )
456 return;
457
458 if ( filter == this || !filter || !filter->enabled() )
459 continue;
460
461 QgsLocatorResult result;
462 result.displayString = filter->activePrefix();
463 result.description = filter->displayName();
464 result.setUserData( QString( filter->activePrefix() + ' ' ) );
465 result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
466 emit resultFetched( result );
467 }
468}
469
470void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
471{
472 mLocator->search( result.getUserData().toString() );
473}
474
475QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
476 : QgsFilterLineEdit( parent )
477 , mLocatorWidget( locator )
478{
479 connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
480}
481
482void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
483{
484 // this adds the completion as grey text at the right of the cursor
485 // see https://stackoverflow.com/a/50425331/1548052
486 // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
487 // this sounds acceptable as it is not very likely to use completion for super long texts
488 // for more details see https://stackoverflow.com/a/54218192/1548052
489
490 QLineEdit::paintEvent( event );
491
492 if ( !hasFocus() )
493 return;
494
495 QString currentText = text();
496
497 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
498 return;
499
500 const QStringList completionList = mLocatorWidget->locator()->completionList();
501
502 mCompletionText.clear();
503 QString completion;
504 for ( const QString &candidate : completionList )
505 {
506 if ( candidate.startsWith( currentText ) )
507 {
508 completion = candidate.right( candidate.length() - currentText.length() );
509 mCompletionText = candidate;
510 break;
511 }
512 }
513
514 if ( completion.isEmpty() )
515 return;
516
517 ensurePolished(); // ensure font() is up to date
518
519 QRect cr = cursorRect();
520 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
521
522 QTextLayout l( completion, font() );
523 l.beginLayout();
524 QTextLine line = l.createLine();
525 line.setLineWidth( width() - pos.x() );
526 line.setPosition( pos );
527 l.endLayout();
528
529 QPainter p( this );
530 p.setPen( QPen( Qt::gray, 1 ) );
531 l.draw( &p, QPoint( 0, 0 ) );
532}
533
534bool QgsLocatorLineEdit::performCompletion()
535{
536 if ( !mCompletionText.isEmpty() )
537 {
538 setText( mCompletionText );
539 mCompletionText.clear();
540 return true;
541 }
542 else
543 return false;
544}
545
546
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
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 getUserData() const
Returns the userData.
QString description
Descriptive text for result.
void setUserData(QVariant userData)
Set userData for the locator 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:62
void searchPrepared()
Emitted when locator has prepared the search (.
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Map canvas is a class for displaying all GIS data types on a canvas.
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.
void transformContextChanged()
Emitted when the project transformContext() is changed.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...