QGIS API Documentation  3.2.0-Bonn (bc43194)
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 
19 #include "qgslocatorwidget.h"
20 #include "qgslocator.h"
21 #include "qgslocatormodel.h"
22 #include "qgsfilterlineedit.h"
23 #include "qgsmapcanvas.h"
24 #include "qgsapplication.h"
25 #include "qgslogger.h"
26 #include <QLayout>
27 #include <QCompleter>
28 #include <QMenu>
29 
31  : QWidget( parent )
32  , mLocator( new QgsLocator( this ) )
33  , mLineEdit( new QgsFilterLineEdit() )
34  , mLocatorModel( new QgsLocatorModel( this ) )
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, ( int )( placeholderMinWidth * 1.6 ) );
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  mProxyModel = new QgsLocatorProxyModel( mLocatorModel );
75  mProxyModel->setSourceModel( mLocatorModel );
76  mResultsView->setModel( mProxyModel );
77  mResultsView->setUniformRowHeights( true );
78  mResultsView->setIconSize( QSize( 16, 16 ) );
79  mResultsView->recalculateSize();
80 
81  connect( mLocator, &QgsLocator::foundResult, this, &QgsLocatorWidget::addResult );
82  connect( mLocator, &QgsLocator::finished, this, &QgsLocatorWidget::searchFinished );
83  connect( mLineEdit, &QLineEdit::textChanged, this, &QgsLocatorWidget::scheduleDelayedPopup );
84  connect( mResultsView, &QAbstractItemView::activated, this, &QgsLocatorWidget::acceptCurrentEntry );
85 
86  // have a tiny delay between typing text in line edit and showing the window
87  mPopupTimer.setInterval( 100 );
88  mPopupTimer.setSingleShot( true );
89  connect( &mPopupTimer, &QTimer::timeout, this, &QgsLocatorWidget::performSearch );
90  mFocusTimer.setInterval( 110 );
91  mFocusTimer.setSingleShot( true );
92  connect( &mFocusTimer, &QTimer::timeout, this, &QgsLocatorWidget::triggerSearchAndShowList );
93 
94  mLineEdit->installEventFilter( this );
95  mResultsContainer->installEventFilter( this );
96  mResultsView->installEventFilter( this );
97  installEventFilter( this );
98  window()->installEventFilter( this );
99 
100  mLocator->registerFilter( new QgsLocatorFilterFilter( this, this ) );
101 
102  mMenu = new QMenu( this );
103  QAction *menuAction = mLineEdit->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) ), QLineEdit::LeadingPosition );
104  connect( menuAction, &QAction::triggered, this, [ = ]
105  {
106  mFocusTimer.stop();
107  mResultsContainer->hide();
108  mMenu->exec( QCursor::pos() );
109  } );
110  connect( mMenu, &QMenu::aboutToShow, this, &QgsLocatorWidget::configMenuAboutToShow );
111 
112 }
113 
115 {
116  return mLocator;
117 }
118 
120 {
121  mMapCanvas = canvas;
122 }
123 
124 void QgsLocatorWidget::search( const QString &string )
125 {
126  mLineEdit->setText( string );
127  window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
128  mLineEdit->setFocus();
129  performSearch();
130 }
131 
133 {
134  mLocator->cancelWithoutBlocking();
135  mLocatorModel->clear();
136  mResultsContainer->hide();
137 }
138 
139 void QgsLocatorWidget::scheduleDelayedPopup()
140 {
141  mPopupTimer.start();
142 }
143 
144 void QgsLocatorWidget::performSearch()
145 {
146  mPopupTimer.stop();
147  updateResults( mLineEdit->text() );
148  showList();
149 }
150 
151 void QgsLocatorWidget::showList()
152 {
153  mResultsContainer->show();
154  mResultsContainer->raise();
155 }
156 
157 void QgsLocatorWidget::triggerSearchAndShowList()
158 {
159  if ( mProxyModel->rowCount() == 0 )
160  performSearch();
161  else
162  showList();
163 }
164 
165 void QgsLocatorWidget::searchFinished()
166 {
167  if ( mHasQueuedRequest )
168  {
169  // a queued request was waiting for this - run the queued search now
170  QString nextSearch = mNextRequestedString;
171  mNextRequestedString.clear();
172  mHasQueuedRequest = false;
173  updateResults( nextSearch );
174  }
175  else
176  {
177  if ( !mLocator->isRunning() )
178  mLineEdit->setShowSpinner( false );
179  }
180 }
181 
182 bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
183 {
184  if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
185  {
186  QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
187  switch ( keyEvent->key() )
188  {
189  case Qt::Key_Up:
190  case Qt::Key_Down:
191  case Qt::Key_PageUp:
192  case Qt::Key_PageDown:
193  triggerSearchAndShowList();
194  mHasSelectedResult = true;
195  QgsApplication::sendEvent( mResultsView, event );
196  return true;
197  case Qt::Key_Home:
198  case Qt::Key_End:
199  if ( keyEvent->modifiers() & Qt::ControlModifier )
200  {
201  triggerSearchAndShowList();
202  mHasSelectedResult = true;
203  QgsApplication::sendEvent( mResultsView, event );
204  return true;
205  }
206  break;
207  case Qt::Key_Enter:
208  case Qt::Key_Return:
209  acceptCurrentEntry();
210  return true;
211  case Qt::Key_Escape:
212  mResultsContainer->hide();
213  return true;
214  case Qt::Key_Tab:
215  mHasSelectedResult = true;
216  mResultsView->selectNextResult();
217  return true;
218  case Qt::Key_Backtab:
219  mHasSelectedResult = true;
220  mResultsView->selectPreviousResult();
221  return true;
222  default:
223  break;
224  }
225  }
226  else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
227  {
228  mHasSelectedResult = true;
229  }
230  else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
231  {
232  if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
233  {
234  mFocusTimer.stop();
235  mResultsContainer->hide();
236  }
237  }
238  else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
239  {
240  mFocusTimer.start();
241  }
242  else if ( obj == window() && event->type() == QEvent::Resize )
243  {
244  mResultsView->recalculateSize();
245  }
246  return QWidget::eventFilter( obj, event );
247 }
248 
249 void QgsLocatorWidget::addResult( const QgsLocatorResult &result )
250 {
251  bool selectFirst = !mHasSelectedResult || mProxyModel->rowCount() == 0;
252  mLocatorModel->addResult( result );
253  if ( selectFirst )
254  {
255  int row = -1;
256  bool selectable = false;
257  while ( !selectable && row < mProxyModel->rowCount() )
258  {
259  row++;
260  selectable = mProxyModel->flags( mProxyModel->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
261  }
262  if ( selectable )
263  mResultsView->setCurrentIndex( mProxyModel->index( row, 0 ) );
264  }
265 }
266 
267 void QgsLocatorWidget::configMenuAboutToShow()
268 {
269  mMenu->clear();
270  for ( QgsLocatorFilter *filter : mLocator->filters() )
271  {
272  if ( !filter->enabled() )
273  continue;
274 
275  QAction *action = new QAction( filter->displayName(), mMenu );
276  connect( action, &QAction::triggered, this, [ = ]
277  {
278  QString currentText = mLineEdit->text();
279  if ( currentText.isEmpty() )
280  currentText = tr( "<type here>" );
281  else
282  {
283  QStringList parts = currentText.split( ' ' );
284  if ( parts.count() > 1 && mLocator->filters( parts.at( 0 ) ).count() > 0 )
285  {
286  parts.pop_front();
287  currentText = parts.join( ' ' );
288  }
289  }
290 
291  mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
292  mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
293  } );
294  mMenu->addAction( action );
295  }
296  mMenu->addSeparator();
297  QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
298  connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
299  mMenu->addAction( configAction );
300 
301 }
302 
303 void QgsLocatorWidget::updateResults( const QString &text )
304 {
305  mLineEdit->setShowSpinner( true );
306  if ( mLocator->isRunning() )
307  {
308  // can't do anything while a query is running, and can't block
309  // here waiting for the current query to cancel
310  // so we queue up this string until cancel has happened
311  mLocator->cancelWithoutBlocking();
312  mNextRequestedString = text;
313  mHasQueuedRequest = true;
314  return;
315  }
316  else
317  {
318  mHasSelectedResult = false;
319  mLocatorModel->deferredClear();
320  mLocator->fetchResults( text, createContext() );
321  }
322 }
323 
324 void QgsLocatorWidget::acceptCurrentEntry()
325 {
326  if ( mHasQueuedRequest )
327  {
328  return;
329  }
330  else
331  {
332  if ( !mResultsView->isVisible() )
333  return;
334 
335  QModelIndex index = mResultsView->currentIndex();
336  if ( !index.isValid() )
337  return;
338 
339  QgsLocatorResult result = mProxyModel->data( index, QgsLocatorModel::ResultDataRole ).value< QgsLocatorResult >();
340  mResultsContainer->hide();
341  mLineEdit->clearFocus();
342  mLocator->clearPreviousResults();
343  result.filter->triggerResult( result );
344  }
345 }
346 
347 QgsLocatorContext QgsLocatorWidget::createContext()
348 {
349  QgsLocatorContext context;
350  if ( mMapCanvas )
351  {
352  context.targetExtent = mMapCanvas->mapSettings().visibleExtent();
353  context.targetExtentCrs = mMapCanvas->mapSettings().destinationCrs();
354  }
355  return context;
356 }
357 
359 
360 //
361 // QgsLocatorResultsView
362 //
363 
364 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
365  : QTreeView( parent )
366 {
367  setRootIsDecorated( false );
368  setUniformRowHeights( true );
369  header()->hide();
370  header()->setStretchLastSection( true );
371 }
372 
373 void QgsLocatorResultsView::recalculateSize()
374 {
375  // try to show about 20 rows
376  int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
377 
378  // try to take up a sensible portion of window width (about half)
379  int width = std::max( 300, window()->size().width() / 2 );
380  QSize newSize( width, rowSize + frameWidth() * 2 );
381  // resize the floating widget this is contained within
382  parentWidget()->resize( newSize );
383  QTreeView::resize( newSize );
384 
385  header()->resizeSection( 0, width / 2 );
386  header()->resizeSection( 1, 0 );
387 }
388 
389 void QgsLocatorResultsView::selectNextResult()
390 {
391  int nextRow = currentIndex().row() + 1;
392  nextRow = nextRow % model()->rowCount( QModelIndex() );
393  setCurrentIndex( model()->index( nextRow, 0 ) );
394 }
395 
396 void QgsLocatorResultsView::selectPreviousResult()
397 {
398  int previousRow = currentIndex().row() - 1;
399  if ( previousRow < 0 )
400  previousRow = model()->rowCount( QModelIndex() ) - 1;
401  setCurrentIndex( model()->index( previousRow, 0 ) );
402 }
403 
404 //
405 // QgsLocatorFilterFilter
406 //
407 
408 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
409  : QgsLocatorFilter( parent )
410  , mLocator( locator )
411 {}
412 
413 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
414 {
415  return new QgsLocatorFilterFilter( mLocator );
416 }
417 
418 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
419 {
421 }
422 
423 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
424 {
425  if ( !string.isEmpty() )
426  {
427  //only shows results when nothing typed
428  return;
429  }
430 
431  for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
432  {
433  if ( feedback->isCanceled() )
434  return;
435 
436  if ( filter == this || !filter || !filter->enabled() )
437  continue;
438 
439  QgsLocatorResult result;
440  result.displayString = filter->activePrefix();
441  result.description = filter->displayName();
442  result.userData = filter->activePrefix() + ' ';
443  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
444  emit resultFetched( result );
445  }
446 }
447 
448 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
449 {
450  mLocator->search( result.userData.toString() );
451 }
452 
453 
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
Definition: qgslocator.cpp:84
QgsLocator * locator()
Returns a pointer to the locator utilized by this widget.
void cancelWithoutBlocking()
Triggers cancelation of any current running query without blocking.
Definition: qgslocator.cpp:229
QIcon icon
Icon for result.
A QWidget subclass for creating widgets which float outside of the normal Qt layout system...
void fetchResults(const QString &string, const QgsLocatorContext &context, QgsFeedback *feedback=nullptr)
Triggers the background fetching of filter results for a specified search string. ...
Definition: qgslocator.cpp:122
A sort proxy model for QgsLocatorModel, which automatically sorts results by precedence.
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.
bool isRunning() const
Returns true if a query is currently being executed by the locator.
Definition: qgslocator.cpp:235
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.
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Definition: qgslocator.cpp:51
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:74
void finished()
Emitted when locator has finished a query, either as a result of successful completion or early cance...
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 cancelation of something running in a worker thread...
Definition: qgsfeedback.h:44
QgsRectangle targetExtent
Map extent to target in results.
virtual void triggerResult(const QgsLocatorResult &result)=0
Triggers a filter result from this filter.
Bottom-left of widget.
QVariant userData
Custom reference or other data set by the filter.
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.
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
void foundResult(const QgsLocatorResult &result)
Emitted whenever a filter encounters a matching result after the fetchResults() method is called...
An abstract list model for displaying the results of locator searches.
QgsCoordinateReferenceSystem targetExtentCrs
Coordinate reference system for the map extent variable.
bool eventFilter(QObject *obj, QEvent *event) override
void deferredClear()
Resets the model and clears all existing results after a short delay, or whenever the next result is ...
const QgsMapSettings & mapSettings() const
Gets access to properties used for map rendering.
QgsLocatorFilter * filter
Filter from which the result was obtained.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
Top-left of widget.
QgsLocatorResult data.
void addResult(const QgsLocatorResult &result)
Adds a new result to the model.
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 clear()
Resets the model and clears all existing results.
void clearPreviousResults()
Will call clearPreviousResults on all filters.
Definition: qgslocator.cpp:240
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...
void invalidateResults()
Invalidates the current search results, e.g.