QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
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 #include <QLayout>
28 #include <QCompleter>
29 #include <QMenu>
30 
32  : QWidget( parent )
33  , mModelBridge( new QgsLocatorModelBridge( this ) )
34  , mLineEdit( new QgsFilterLineEdit() )
35  , mResultsView( new QgsLocatorResultsView() )
36 {
37  mLineEdit->setShowClearButton( true );
38 #ifdef Q_OS_MACX
39  mLineEdit->setPlaceholderText( tr( "Type to locate (⌘K)" ) );
40 #else
41  mLineEdit->setPlaceholderText( tr( "Type to locate (Ctrl+K)" ) );
42 #endif
43 
44  int placeholderMinWidth = mLineEdit->fontMetrics().width( mLineEdit->placeholderText() );
45  int minWidth = std::max( 200, static_cast< int >( placeholderMinWidth * 1.8 ) );
46  resize( minWidth, 30 );
47  QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
48  sizePolicy.setHorizontalStretch( 0 );
49  sizePolicy.setVerticalStretch( 0 );
50  setSizePolicy( sizePolicy );
51  setMinimumSize( QSize( minWidth, 0 ) );
52 
53  QHBoxLayout *layout = new QHBoxLayout();
54  layout->setMargin( 0 );
55  layout->setContentsMargins( 0, 0, 0, 0 );
56  layout->addWidget( mLineEdit );
57  setLayout( layout );
58 
59  setFocusProxy( mLineEdit );
60 
61  // setup floating container widget
62  mResultsContainer = new QgsFloatingWidget( parent ? parent->window() : nullptr );
63  mResultsContainer->setAnchorWidget( mLineEdit );
64  mResultsContainer->setAnchorPoint( QgsFloatingWidget::BottomLeft );
66 
67  QHBoxLayout *containerLayout = new QHBoxLayout();
68  containerLayout->setMargin( 0 );
69  containerLayout->setContentsMargins( 0, 0, 0, 0 );
70  containerLayout->addWidget( mResultsView );
71  mResultsContainer->setLayout( containerLayout );
72  mResultsContainer->hide();
73 
74  mResultsView->setModel( mModelBridge->proxyModel() );
75  mResultsView->setUniformRowHeights( true );
76 
78  mResultsView->setIconSize( QSize( iconSize, iconSize ) );
79  mResultsView->recalculateSize();
80  mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
81 
82  connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup );
83  connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry );
84  connect( mResultsView, &QAbstractItemView::customContextMenuRequested, this, &QgsLocatorWidget::showContextMenu );
85 
86  connect( mModelBridge, &QgsLocatorModelBridge::resultAdded, this, &QgsLocatorWidget::resultAdded );
87  connect( mModelBridge, &QgsLocatorModelBridge::isRunningChanged, this, [ = ]() {mLineEdit->setShowSpinner( mModelBridge->isRunning() );} );
88  connect( mModelBridge, & QgsLocatorModelBridge::resultsCleared, this, [ = ]() {mHasSelectedResult = false;} );
89 
90  // have a tiny delay between typing text in line edit and showing the window
91  mPopupTimer.setInterval( 100 );
92  mPopupTimer.setSingleShot( true );
93  connect( &mPopupTimer, &QTimer::timeout, this, &QgsLocatorWidget::performSearch );
94  mFocusTimer.setInterval( 110 );
95  mFocusTimer.setSingleShot( true );
96  connect( &mFocusTimer, &QTimer::timeout, this, &QgsLocatorWidget::triggerSearchAndShowList );
97 
98  mLineEdit->installEventFilter( this );
99  mResultsContainer->installEventFilter( this );
100  mResultsView->installEventFilter( this );
101  installEventFilter( this );
102  window()->installEventFilter( this );
103 
104  mModelBridge->locator()->registerFilter( new QgsLocatorFilterFilter( this, this ) );
105 
106  mMenu = new QMenu( this );
107  QAction *menuAction = mLineEdit->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) ), QLineEdit::LeadingPosition );
108  connect( menuAction, &QAction::triggered, this, [ = ]
109  {
110  mFocusTimer.stop();
111  mResultsContainer->hide();
112  mMenu->exec( QCursor::pos() );
113  } );
114  connect( mMenu, &QMenu::aboutToShow, this, &QgsLocatorWidget::configMenuAboutToShow );
115 
116 }
117 
119 {
120  return mModelBridge->locator();
121 }
122 
124 {
125  if ( mMapCanvas == canvas )
126  return;
127 
128  for ( const QMetaObject::Connection &conn : qgis::as_const( mCanvasConnections ) )
129  {
130  disconnect( conn );
131  }
132  mCanvasConnections.clear();
133 
134  mMapCanvas = canvas;
135  if ( mMapCanvas )
136  {
137  mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );
138  mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );
139  mCanvasConnections
140  << connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, [ = ]() {mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );} )
141  << connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, [ = ]() {mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );} ) ;
142  }
143 }
144 
145 void QgsLocatorWidget::search( const QString &string )
146 {
147  mLineEdit->setText( string );
148  window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
149  scheduleDelayedPopup();
150  mLineEdit->setFocus();
151  performSearch();
152 }
153 
155 {
156  mModelBridge->invalidateResults();
157  mResultsContainer->hide();
158 }
159 
160 void QgsLocatorWidget::scheduleDelayedPopup()
161 {
162  mPopupTimer.start();
163 }
164 
165 void QgsLocatorWidget::resultAdded()
166 {
167  bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
168  if ( selectFirst )
169  {
170  int row = -1;
171  bool selectable = false;
172  while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
173  {
174  row++;
175  selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
176  }
177  if ( selectable )
178  mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
179  }
180 }
181 
182 void QgsLocatorWidget::showContextMenu( const QPoint &point )
183 {
184  QModelIndex index = mResultsView->indexAt( point );
185  if ( !index.isValid() )
186  return;
187 
188  const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
189  QMenu *contextMenu = new QMenu( mResultsView );
190  for ( auto resultAction : actions )
191  {
192  QAction *menuAction = new QAction( resultAction.text, contextMenu );
193  if ( !resultAction.iconPath.isEmpty() )
194  menuAction->setIcon( QIcon( resultAction.iconPath ) );
195  connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
196  contextMenu->addAction( menuAction );
197  }
198  contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
199 }
200 
201 void QgsLocatorWidget::performSearch()
202 {
203  mPopupTimer.stop();
204  mModelBridge->performSearch( mLineEdit->text() );
205  showList();
206 }
207 
208 void QgsLocatorWidget::showList()
209 {
210  mResultsContainer->show();
211  mResultsContainer->raise();
212 }
213 
214 void QgsLocatorWidget::triggerSearchAndShowList()
215 {
216  if ( mModelBridge->proxyModel()->rowCount() == 0 )
217  performSearch();
218  else
219  showList();
220 }
221 
222 bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
223 {
224  if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
225  {
226  QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
227  switch ( keyEvent->key() )
228  {
229  case Qt::Key_Up:
230  case Qt::Key_Down:
231  case Qt::Key_PageUp:
232  case Qt::Key_PageDown:
233  triggerSearchAndShowList();
234  mHasSelectedResult = true;
235  QgsApplication::sendEvent( mResultsView, event );
236  return true;
237  case Qt::Key_Home:
238  case Qt::Key_End:
239  if ( keyEvent->modifiers() & Qt::ControlModifier )
240  {
241  triggerSearchAndShowList();
242  mHasSelectedResult = true;
243  QgsApplication::sendEvent( mResultsView, event );
244  return true;
245  }
246  break;
247  case Qt::Key_Enter:
248  case Qt::Key_Return:
249  acceptCurrentEntry();
250  return true;
251  case Qt::Key_Escape:
252  mResultsContainer->hide();
253  return true;
254  case Qt::Key_Tab:
255  mHasSelectedResult = true;
256  mResultsView->selectNextResult();
257  return true;
258  case Qt::Key_Backtab:
259  mHasSelectedResult = true;
260  mResultsView->selectPreviousResult();
261  return true;
262  default:
263  break;
264  }
265  }
266  else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
267  {
268  mHasSelectedResult = true;
269  }
270  else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
271  {
272  if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
273  {
274  mFocusTimer.stop();
275  mResultsContainer->hide();
276  }
277  }
278  else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
279  {
280  mFocusTimer.start();
281  }
282  else if ( obj == window() && event->type() == QEvent::Resize )
283  {
284  mResultsView->recalculateSize();
285  }
286  return QWidget::eventFilter( obj, event );
287 }
288 
289 void QgsLocatorWidget::configMenuAboutToShow()
290 {
291  mMenu->clear();
292  for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
293  {
294  if ( !filter->enabled() )
295  continue;
296 
297  QAction *action = new QAction( filter->displayName(), mMenu );
298  connect( action, &QAction::triggered, this, [ = ]
299  {
300  QString currentText = mLineEdit->text();
301  if ( currentText.isEmpty() )
302  currentText = tr( "<type here>" );
303  else
304  {
305  QStringList parts = currentText.split( ' ' );
306  if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
307  {
308  parts.pop_front();
309  currentText = parts.join( ' ' );
310  }
311  }
312 
313  mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
314  mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
315  } );
316  mMenu->addAction( action );
317  }
318  mMenu->addSeparator();
319  QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
320  connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
321  mMenu->addAction( configAction );
322 }
323 
324 
325 
326 void QgsLocatorWidget::acceptCurrentEntry()
327 {
328  if ( mModelBridge->hasQueueRequested() )
329  {
330  return;
331  }
332  else
333  {
334  if ( !mResultsView->isVisible() )
335  return;
336 
337  QModelIndex index = mResultsView->currentIndex();
338  if ( !index.isValid() )
339  return;
340 
341  mResultsContainer->hide();
342  mLineEdit->clearFocus();
343  mModelBridge->triggerResult( index );
344  }
345 }
346 
347 
348 
350 
351 //
352 // QgsLocatorResultsView
353 //
354 
355 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
356  : QTreeView( parent )
357 {
358  setRootIsDecorated( false );
359  setUniformRowHeights( true );
360  header()->hide();
361  header()->setStretchLastSection( true );
362 }
363 
364 void QgsLocatorResultsView::recalculateSize()
365 {
366  // try to show about 20 rows
367  int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
368 
369  // try to take up a sensible portion of window width (about half)
370  int width = std::max( 300, window()->size().width() / 2 );
371  QSize newSize( width, rowSize + frameWidth() * 2 );
372  // resize the floating widget this is contained within
373  parentWidget()->resize( newSize );
374  QTreeView::resize( newSize );
375 
376  header()->resizeSection( 0, width / 2 );
377  header()->resizeSection( 1, 0 );
378 }
379 
380 void QgsLocatorResultsView::selectNextResult()
381 {
382  int nextRow = currentIndex().row() + 1;
383  nextRow = nextRow % model()->rowCount( QModelIndex() );
384  setCurrentIndex( model()->index( nextRow, 0 ) );
385 }
386 
387 void QgsLocatorResultsView::selectPreviousResult()
388 {
389  int previousRow = currentIndex().row() - 1;
390  if ( previousRow < 0 )
391  previousRow = model()->rowCount( QModelIndex() ) - 1;
392  setCurrentIndex( model()->index( previousRow, 0 ) );
393 }
394 
395 //
396 // QgsLocatorFilterFilter
397 //
398 
399 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
400  : QgsLocatorFilter( parent )
401  , mLocator( locator )
402 {}
403 
404 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
405 {
406  return new QgsLocatorFilterFilter( mLocator );
407 }
408 
409 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
410 {
412 }
413 
414 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
415 {
416  if ( !string.isEmpty() )
417  {
418  //only shows results when nothing typed
419  return;
420  }
421 
422  for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
423  {
424  if ( feedback->isCanceled() )
425  return;
426 
427  if ( filter == this || !filter || !filter->enabled() )
428  continue;
429 
430  QgsLocatorResult result;
431  result.displayString = filter->activePrefix();
432  result.description = filter->displayName();
433  result.userData = filter->activePrefix() + ' ';
434  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
435  emit resultFetched( result );
436  }
437 }
438 
439 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
440 {
441  mLocator->search( result.userData.toString() );
442 }
443 
444 
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
Definition: qgslocator.cpp:86
void updateCanvasCrs(const QgsCoordinateReferenceSystem &crs)
Update the canvas CRS used to create search context.
QgsLocator * locator()
Returns a pointer to the locator utilized by this widget.
QIcon icon
Icon for result.
A QWidget subclass for creating widgets which float outside of the normal Qt layout system...
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly...
Q_INVOKABLE void performSearch(const QString &text)
Perform a search.
The actions to be shown for the given result in a context menu.
void setShowSpinner(bool showSpinner)
Show a spinner icon.
void setMapCanvas(QgsMapCanvas *canvas)
Sets a map canvas to associate with the widget.
static QIcon getThemeIcon(const QString &name)
Helper to get a theme icon.
QgsLocatorWidget(QWidget *parent SIP_TRANSFERTHIS=nullptr)
Constructor for QgsLocatorWidget.
void configTriggered()
Emitted when the configure option is triggered in the widget.
QString description
Descriptive text for result.
The QgsLocatorModelBridge class provides the core functionality to be used in a locator widget...
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Definition: qgslocator.cpp:53
void updateCanvasExtent(const QgsRectangle &extent)
Update the canvas extent used to create search context.
QgsRectangle visibleExtent() const
Returns the actual extent derived from requested extent that takes takes output image size into accou...
Map canvas is a class for displaying all GIS data types on a canvas.
Definition: qgsmapcanvas.h:75
QgsCoordinateReferenceSystem destinationCrs() const
returns CRS of destination coordinate reference system
void search(const QString &string)
Triggers the locator widget to focus, open and start searching for a specified string.
Base class for feedback objects to be used for cancellation of something running in a worker thread...
Definition: qgsfeedback.h:44
Bottom-left of widget.
QVariant userData
Custom reference or other data set by the filter.
void isRunningChanged()
Emitted when the running status changes.
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 setAnchorWidget(QWidget *widget)
Sets the widget to "anchor" the floating widget to.
QString displayString
String displayed for result.
QLineEdit subclass with built in support for clearing the widget&#39;s value and handling custom null val...
Encapsulates the properties relating to the context of a locator search.
void destinationCrsChanged()
Emitted when map CRS has changed.
Encapsulates properties of an individual matching result found by a QgsLocatorFilter.
Abstract base class for filters which collect locator results.
Handles the management of QgsLocatorFilter objects and async collection of search results from them...
Definition: qgslocator.h:57
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window&#39;s toolbar icons.
void resultsCleared()
Emitted when the results are cleared.
bool eventFilter(QObject *obj, QEvent *event) override
const QgsMapSettings & mapSettings() const
Gets access to properties used for map rendering.
Q_INVOKABLE QgsLocatorProxyModel * proxyModel() const
Returns the proxy model.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
void invalidateResults()
This will invalidate current search results.
Top-left of widget.
bool hasQueueRequested() const
Returns true if some text to be search is pending in the queue.
Filter finds results quickly and can be safely run in the main thread.
A special locator widget which allows searching for matching results from a QgsLocator and presenting...
void setShowClearButton(bool visible)
Sets whether the widget&#39;s clear button is visible.
void extentsChanged()
Emitted when the extents of the map change.
void setAnchorWidgetPoint(AnchorPoint point)
Returns the anchor widget&#39;s anchor point, which corresponds to the point on the anchor widget which t...
void setAnchorPoint(AnchorPoint point)
Sets the floating widget&#39;s anchor point, which corresponds to the point on the widget which should re...
QgsLocator * locator() const
Returns the locator.
void invalidateResults()
Invalidates the current search results, e.g.
void resultAdded()
Emitted when a result is added.