QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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  mLineEdit->setFocus();
150  performSearch();
151 }
152 
154 {
155  mModelBridge->invalidateResults();
156  mResultsContainer->hide();
157 }
158 
159 void QgsLocatorWidget::scheduleDelayedPopup()
160 {
161  mPopupTimer.start();
162 }
163 
164 void QgsLocatorWidget::resultAdded()
165 {
166  bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
167  if ( selectFirst )
168  {
169  int row = -1;
170  bool selectable = false;
171  while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
172  {
173  row++;
174  selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
175  }
176  if ( selectable )
177  mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
178  }
179 }
180 
181 void QgsLocatorWidget::showContextMenu( const QPoint &point )
182 {
183  QModelIndex index = mResultsView->indexAt( point );
184  if ( !index.isValid() )
185  return;
186 
187  const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
188  QMenu *contextMenu = new QMenu( mResultsView );
189  for ( auto resultAction : actions )
190  {
191  QAction *menuAction = new QAction( resultAction.text, contextMenu );
192  if ( !resultAction.iconPath.isEmpty() )
193  menuAction->setIcon( QIcon( resultAction.iconPath ) );
194  connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
195  contextMenu->addAction( menuAction );
196  }
197  contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
198 }
199 
200 void QgsLocatorWidget::performSearch()
201 {
202  mPopupTimer.stop();
203  mModelBridge->performSearch( mLineEdit->text() );
204  showList();
205 }
206 
207 void QgsLocatorWidget::showList()
208 {
209  mResultsContainer->show();
210  mResultsContainer->raise();
211 }
212 
213 void QgsLocatorWidget::triggerSearchAndShowList()
214 {
215  if ( mModelBridge->proxyModel()->rowCount() == 0 )
216  performSearch();
217  else
218  showList();
219 }
220 
221 bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
222 {
223  if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
224  {
225  QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
226  switch ( keyEvent->key() )
227  {
228  case Qt::Key_Up:
229  case Qt::Key_Down:
230  case Qt::Key_PageUp:
231  case Qt::Key_PageDown:
232  triggerSearchAndShowList();
233  mHasSelectedResult = true;
234  QgsApplication::sendEvent( mResultsView, event );
235  return true;
236  case Qt::Key_Home:
237  case Qt::Key_End:
238  if ( keyEvent->modifiers() & Qt::ControlModifier )
239  {
240  triggerSearchAndShowList();
241  mHasSelectedResult = true;
242  QgsApplication::sendEvent( mResultsView, event );
243  return true;
244  }
245  break;
246  case Qt::Key_Enter:
247  case Qt::Key_Return:
248  acceptCurrentEntry();
249  return true;
250  case Qt::Key_Escape:
251  mResultsContainer->hide();
252  return true;
253  case Qt::Key_Tab:
254  mHasSelectedResult = true;
255  mResultsView->selectNextResult();
256  return true;
257  case Qt::Key_Backtab:
258  mHasSelectedResult = true;
259  mResultsView->selectPreviousResult();
260  return true;
261  default:
262  break;
263  }
264  }
265  else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
266  {
267  mHasSelectedResult = true;
268  }
269  else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
270  {
271  if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
272  {
273  mFocusTimer.stop();
274  mResultsContainer->hide();
275  }
276  }
277  else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
278  {
279  mFocusTimer.start();
280  }
281  else if ( obj == window() && event->type() == QEvent::Resize )
282  {
283  mResultsView->recalculateSize();
284  }
285  return QWidget::eventFilter( obj, event );
286 }
287 
288 void QgsLocatorWidget::configMenuAboutToShow()
289 {
290  mMenu->clear();
291  for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
292  {
293  if ( !filter->enabled() )
294  continue;
295 
296  QAction *action = new QAction( filter->displayName(), mMenu );
297  connect( action, &QAction::triggered, this, [ = ]
298  {
299  QString currentText = mLineEdit->text();
300  if ( currentText.isEmpty() )
301  currentText = tr( "<type here>" );
302  else
303  {
304  QStringList parts = currentText.split( ' ' );
305  if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
306  {
307  parts.pop_front();
308  currentText = parts.join( ' ' );
309  }
310  }
311 
312  mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
313  mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
314  } );
315  mMenu->addAction( action );
316  }
317  mMenu->addSeparator();
318  QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
319  connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
320  mMenu->addAction( configAction );
321 }
322 
323 
324 
325 void QgsLocatorWidget::acceptCurrentEntry()
326 {
327  if ( mModelBridge->hasQueueRequested() )
328  {
329  return;
330  }
331  else
332  {
333  if ( !mResultsView->isVisible() )
334  return;
335 
336  QModelIndex index = mResultsView->currentIndex();
337  if ( !index.isValid() )
338  return;
339 
340  mResultsContainer->hide();
341  mLineEdit->clearFocus();
342  mModelBridge->triggerResult( index );
343  }
344 }
345 
346 
347 
349 
350 //
351 // QgsLocatorResultsView
352 //
353 
354 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
355  : QTreeView( parent )
356 {
357  setRootIsDecorated( false );
358  setUniformRowHeights( true );
359  header()->hide();
360  header()->setStretchLastSection( true );
361 }
362 
363 void QgsLocatorResultsView::recalculateSize()
364 {
365  // try to show about 20 rows
366  int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
367 
368  // try to take up a sensible portion of window width (about half)
369  int width = std::max( 300, window()->size().width() / 2 );
370  QSize newSize( width, rowSize + frameWidth() * 2 );
371  // resize the floating widget this is contained within
372  parentWidget()->resize( newSize );
373  QTreeView::resize( newSize );
374 
375  header()->resizeSection( 0, width / 2 );
376  header()->resizeSection( 1, 0 );
377 }
378 
379 void QgsLocatorResultsView::selectNextResult()
380 {
381  int nextRow = currentIndex().row() + 1;
382  nextRow = nextRow % model()->rowCount( QModelIndex() );
383  setCurrentIndex( model()->index( nextRow, 0 ) );
384 }
385 
386 void QgsLocatorResultsView::selectPreviousResult()
387 {
388  int previousRow = currentIndex().row() - 1;
389  if ( previousRow < 0 )
390  previousRow = model()->rowCount( QModelIndex() ) - 1;
391  setCurrentIndex( model()->index( previousRow, 0 ) );
392 }
393 
394 //
395 // QgsLocatorFilterFilter
396 //
397 
398 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
399  : QgsLocatorFilter( parent )
400  , mLocator( locator )
401 {}
402 
403 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
404 {
405  return new QgsLocatorFilterFilter( mLocator );
406 }
407 
408 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
409 {
411 }
412 
413 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
414 {
415  if ( !string.isEmpty() )
416  {
417  //only shows results when nothing typed
418  return;
419  }
420 
421  for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
422  {
423  if ( feedback->isCanceled() )
424  return;
425 
426  if ( filter == this || !filter || !filter->enabled() )
427  continue;
428 
429  QgsLocatorResult result;
430  result.displayString = filter->activePrefix();
431  result.description = filter->displayName();
432  result.userData = filter->activePrefix() + ' ';
433  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
434  emit resultFetched( result );
435  }
436 }
437 
438 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
439 {
440  mLocator->search( result.userData.toString() );
441 }
442 
443 
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:73
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.