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