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