QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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  QStyleOptionViewItem optView;
382 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
383  optView.init( this );
384 #else
385  optView.initFrom( this );
386 #endif
387 
388  // try to show about 20 rows
389  int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
390 
391  // try to take up a sensible portion of window width (about half)
392  int width = std::max( 300, window()->size().width() / 2 );
393  QSize newSize( width, rowSize + frameWidth() * 2 );
394  // resize the floating widget this is contained within
395  parentWidget()->resize( newSize );
396  QTreeView::resize( newSize );
397 
398  header()->resizeSection( 0, width / 2 );
399  header()->resizeSection( 1, 0 );
400 }
401 
402 void QgsLocatorResultsView::selectNextResult()
403 {
404  const int rowCount = model()->rowCount( QModelIndex() );
405  if ( rowCount == 0 )
406  return;
407 
408  int nextRow = currentIndex().row() + 1;
409  nextRow = nextRow % rowCount;
410  setCurrentIndex( model()->index( nextRow, 0 ) );
411 }
412 
413 void QgsLocatorResultsView::selectPreviousResult()
414 {
415  const int rowCount = model()->rowCount( QModelIndex() );
416  if ( rowCount == 0 )
417  return;
418 
419  int previousRow = currentIndex().row() - 1;
420  if ( previousRow < 0 )
421  previousRow = rowCount - 1;
422  setCurrentIndex( model()->index( previousRow, 0 ) );
423 }
424 
425 //
426 // QgsLocatorFilterFilter
427 //
428 
429 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
430  : QgsLocatorFilter( parent )
431  , mLocator( locator )
432 {}
433 
434 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
435 {
436  return new QgsLocatorFilterFilter( mLocator );
437 }
438 
439 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
440 {
442 }
443 
444 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
445 {
446  if ( !string.isEmpty() )
447  {
448  //only shows results when nothing typed
449  return;
450  }
451 
452  for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
453  {
454  if ( feedback->isCanceled() )
455  return;
456 
457  if ( filter == this || !filter || !filter->enabled() )
458  continue;
459 
460  QgsLocatorResult result;
461  result.displayString = filter->activePrefix();
462  result.description = filter->displayName();
463  result.userData = QString( filter->activePrefix() + ' ' );
464  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
465  emit resultFetched( result );
466  }
467 }
468 
469 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
470 {
471  mLocator->search( result.userData.toString() );
472 }
473 
474 QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent )
475  : QgsFilterLineEdit( parent )
476  , mLocatorWidget( locator )
477 {
478  connect( mLocatorWidget->locator(), &QgsLocator::searchPrepared, this, [&] { update(); } );
479 }
480 
481 void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
482 {
483  // this adds the completion as grey text at the right of the cursor
484  // see https://stackoverflow.com/a/50425331/1548052
485  // this is possible that the completion might be badly rendered if the cursor is larger than the line edit
486  // this sounds acceptable as it is not very likely to use completion for super long texts
487  // for more details see https://stackoverflow.com/a/54218192/1548052
488 
489  QLineEdit::paintEvent( event );
490 
491  if ( !hasFocus() )
492  return;
493 
494  QString currentText = text();
495 
496  if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
497  return;
498 
499  const QStringList completionList = mLocatorWidget->locator()->completionList();
500 
501  mCompletionText.clear();
502  QString completion;
503  for ( const QString &candidate : completionList )
504  {
505  if ( candidate.startsWith( currentText ) )
506  {
507  completion = candidate.right( candidate.length() - currentText.length() );
508  mCompletionText = candidate;
509  break;
510  }
511  }
512 
513  if ( completion.isEmpty() )
514  return;
515 
516  ensurePolished(); // ensure font() is up to date
517 
518  QRect cr = cursorRect();
519  QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
520 
521  QTextLayout l( completion, font() );
522  l.beginLayout();
523  QTextLine line = l.createLine();
524  line.setLineWidth( width() - pos.x() );
525  line.setPosition( pos );
526  l.endLayout();
527 
528  QPainter p( this );
529  p.setPen( QPen( Qt::gray, 1 ) );
530  l.draw( &p, QPoint( 0, 0 ) );
531 }
532 
533 bool QgsLocatorLineEdit::performCompletion()
534 {
535  if ( !mCompletionText.isEmpty() )
536  {
537  setText( mCompletionText );
538  mCompletionText.clear();
539  return true;
540  }
541  else
542  return false;
543 }
544 
545 
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:89
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.
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,...