QGIS API Documentation  3.20.0-Odense (decaadbb31)
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"
21 #include "qgslocatormodelbridge.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 );
66  mResultsContainer->setAnchorPoint( QgsFloatingWidget::BottomLeft );
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 
152 void 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 
175 void QgsLocatorWidget::scheduleDelayedPopup()
176 {
177  mPopupTimer.start();
178 }
179 
180 void 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 
197 void 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 
216 void QgsLocatorWidget::performSearch()
217 {
218  mPopupTimer.stop();
219  mModelBridge->performSearch( mLineEdit->text() );
220  showList();
221 }
222 
223 void QgsLocatorWidget::showList()
224 {
225  mResultsContainer->show();
226  mResultsContainer->raise();
227 }
228 
229 void QgsLocatorWidget::triggerSearchAndShowList()
230 {
231  if ( mModelBridge->proxyModel()->rowCount() == 0 )
232  performSearch();
233  else
234  showList();
235 }
236 
237 bool 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 
307 void 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 
343 void 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 
370 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
371  : QTreeView( parent )
372 {
373  setRootIsDecorated( false );
374  setUniformRowHeights( true );
375  header()->hide();
376  header()->setStretchLastSection( true );
377 }
378 
379 void QgsLocatorResultsView::recalculateSize()
380 {
381  // try to show about 20 rows
382  int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
383 
384  // try to take up a sensible portion of window width (about half)
385  int width = std::max( 300, window()->size().width() / 2 );
386  QSize newSize( width, rowSize + frameWidth() * 2 );
387  // resize the floating widget this is contained within
388  parentWidget()->resize( newSize );
389  QTreeView::resize( newSize );
390 
391  header()->resizeSection( 0, width / 2 );
392  header()->resizeSection( 1, 0 );
393 }
394 
395 void QgsLocatorResultsView::selectNextResult()
396 {
397  int nextRow = currentIndex().row() + 1;
398  nextRow = nextRow % model()->rowCount( QModelIndex() );
399  setCurrentIndex( model()->index( nextRow, 0 ) );
400 }
401 
402 void QgsLocatorResultsView::selectPreviousResult()
403 {
404  int previousRow = currentIndex().row() - 1;
405  if ( previousRow < 0 )
406  previousRow = model()->rowCount( QModelIndex() ) - 1;
407  setCurrentIndex( model()->index( previousRow, 0 ) );
408 }
409 
410 //
411 // QgsLocatorFilterFilter
412 //
413 
414 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
415  : QgsLocatorFilter( parent )
416  , mLocator( locator )
417 {}
418 
419 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
420 {
421  return new QgsLocatorFilterFilter( mLocator );
422 }
423 
424 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
425 {
427 }
428 
429 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
430 {
431  if ( !string.isEmpty() )
432  {
433  //only shows results when nothing typed
434  return;
435  }
436 
437  for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
438  {
439  if ( feedback->isCanceled() )
440  return;
441 
442  if ( filter == this || !filter || !filter->enabled() )
443  continue;
444 
445  QgsLocatorResult result;
446  result.displayString = filter->activePrefix();
447  result.description = filter->displayName();
448  result.userData = QString( filter->activePrefix() + ' ' );
449  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
450  emit resultFetched( result );
451  }
452 }
453 
454 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
455 {
456  mLocator->search( result.userData.toString() );
457 }
458 
459 QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
460  : QgsFilterLineEdit( parent )
461  , mLocatorWidget( locator )
462 {
463  connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
464 }
465 
466 void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
467 {
468  // this adds the completion as grey text at the right of the cursor
469  // see https://stackoverflow.com/a/50425331/1548052
470  // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
471  // this sounds acceptable as it is not very likely to use completion for super long texts
472  // for more details see https://stackoverflow.com/a/54218192/1548052
473 
474  QLineEdit::paintEvent( event );
475 
476  if ( !hasFocus() )
477  return;
478 
479  QString currentText = text();
480 
481  if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
482  return;
483 
484  const QStringList completionList = mLocatorWidget->locator()->completionList();
485 
486  mCompletionText.clear();
487  QString completion;
488  for ( const QString &candidate : completionList )
489  {
490  if ( candidate.startsWith( currentText ) )
491  {
492  completion = candidate.right( candidate.length() - currentText.length() );
493  mCompletionText = candidate;
494  break;
495  }
496  }
497 
498  if ( completion.isEmpty() )
499  return;
500 
501  ensurePolished(); // ensure font() is up to date
502 
503  QRect cr = cursorRect();
504  QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
505 
506  QTextLayout l( completion, font() );
507  l.beginLayout();
508  QTextLine line = l.createLine();
509  line.setLineWidth( width() - pos.x() );
510  line.setPosition( pos );
511  l.endLayout();
512 
513  QPainter p( this );
514  p.setPen( QPen( Qt::gray, 1 ) );
515  l.draw( &p, QPoint( 0, 0 ) );
516 }
517 
518 bool QgsLocatorLineEdit::performCompletion()
519 {
520  if ( !mCompletionText.isEmpty() )
521  {
522  setText( mCompletionText );
523  mCompletionText.clear();
524  return true;
525  }
526  else
527  return false;
528 }
529 
530 
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:92
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Definition: qgslocator.cpp:55
Map canvas is a class for displaying all GIS data types on a canvas.
Definition: qgsmapcanvas.h:86
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 takes output image size into accou...
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system for the map render.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:467
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,...