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