QGIS API Documentation  3.0.2-Girona (307d082)
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 = mProxyModel->flags( mProxyModel->index( 0, 0 ) ) & Qt::ItemIsSelectable ? 0 : 1;
256  mResultsView->setCurrentIndex( mProxyModel->index( row, 0 ) );
257  }
258 }
259 
260 void QgsLocatorWidget::configMenuAboutToShow()
261 {
262  mMenu->clear();
263  QMap< QString, QgsLocatorFilter *> filters = mLocator->prefixedFilters();
264  QMap< QString, QgsLocatorFilter *>::const_iterator fIt = filters.constBegin();
265  for ( ; fIt != filters.constEnd(); ++fIt )
266  {
267  if ( !fIt.value()->enabled() )
268  continue;
269 
270  QAction *action = new QAction( fIt.value()->displayName(), mMenu );
271  connect( action, &QAction::triggered, this, [ = ]
272  {
273  QString currentText = mLineEdit->text();
274  if ( currentText.isEmpty() )
275  currentText = tr( "<type here>" );
276  else
277  {
278  QStringList parts = currentText.split( ' ' );
279  if ( parts.count() > 1 && mLocator->prefixedFilters().contains( parts.at( 0 ) ) )
280  {
281  parts.pop_front();
282  currentText = parts.join( ' ' );
283  }
284  }
285 
286  mLineEdit->setText( fIt.key() + ' ' + currentText );
287  mLineEdit->setSelection( fIt.key().length() + 1, currentText.length() );
288  } );
289  mMenu->addAction( action );
290  }
291  mMenu->addSeparator();
292  QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
293  connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
294  mMenu->addAction( configAction );
295 
296 }
297 
298 void QgsLocatorWidget::updateResults( const QString &text )
299 {
300  mLineEdit->setShowSpinner( true );
301  if ( mLocator->isRunning() )
302  {
303  // can't do anything while a query is running, and can't block
304  // here waiting for the current query to cancel
305  // so we queue up this string until cancel has happened
306  mLocator->cancelWithoutBlocking();
307  mNextRequestedString = text;
308  mHasQueuedRequest = true;
309  return;
310  }
311  else
312  {
313  mHasSelectedResult = false;
314  mLocatorModel->deferredClear();
315  mLocator->fetchResults( text, createContext() );
316  }
317 }
318 
319 void QgsLocatorWidget::acceptCurrentEntry()
320 {
321  if ( mHasQueuedRequest )
322  {
323  return;
324  }
325  else
326  {
327  if ( !mResultsView->isVisible() )
328  return;
329 
330  QModelIndex index = mResultsView->currentIndex();
331  if ( !index.isValid() )
332  return;
333 
334  QgsLocatorResult result = mProxyModel->data( index, QgsLocatorModel::ResultDataRole ).value< QgsLocatorResult >();
335  mResultsContainer->hide();
336  mLineEdit->clearFocus();
337  result.filter->triggerResult( result );
338  }
339 }
340 
341 QgsLocatorContext QgsLocatorWidget::createContext()
342 {
343  QgsLocatorContext context;
344  if ( mMapCanvas )
345  {
346  context.targetExtent = mMapCanvas->mapSettings().visibleExtent();
347  context.targetExtentCrs = mMapCanvas->mapSettings().destinationCrs();
348  }
349  return context;
350 }
351 
353 
354 //
355 // QgsLocatorResultsView
356 //
357 
358 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
359  : QTreeView( parent )
360 {
361  setRootIsDecorated( false );
362  setUniformRowHeights( true );
363  header()->hide();
364  header()->setStretchLastSection( true );
365 }
366 
367 void QgsLocatorResultsView::recalculateSize()
368 {
369  // try to show about 20 rows
370  int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
371 
372  // try to take up a sensible portion of window width (about half)
373  int width = std::max( 300, window()->size().width() / 2 );
374  QSize newSize( width, rowSize + frameWidth() * 2 );
375  // resize the floating widget this is contained within
376  parentWidget()->resize( newSize );
377  QTreeView::resize( newSize );
378 
379  header()->resizeSection( 0, width / 2 );
380  header()->resizeSection( 1, 0 );
381 }
382 
383 void QgsLocatorResultsView::selectNextResult()
384 {
385  int nextRow = currentIndex().row() + 1;
386  nextRow = nextRow % model()->rowCount( QModelIndex() );
387  setCurrentIndex( model()->index( nextRow, 0 ) );
388 }
389 
390 void QgsLocatorResultsView::selectPreviousResult()
391 {
392  int previousRow = currentIndex().row() - 1;
393  if ( previousRow < 0 )
394  previousRow = model()->rowCount( QModelIndex() ) - 1;
395  setCurrentIndex( model()->index( previousRow, 0 ) );
396 }
397 
398 //
399 // QgsLocatorFilterFilter
400 //
401 
402 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
403  : QgsLocatorFilter( parent )
404  , mLocator( locator )
405 {}
406 
407 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
408 {
409  return new QgsLocatorFilterFilter( mLocator );
410 }
411 
412 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
413 {
415 }
416 
417 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
418 {
419  if ( !string.isEmpty() )
420  {
421  //only shows results when nothing typed
422  return;
423  }
424 
425  QMap< QString, QgsLocatorFilter *> filters = mLocator->locator()->prefixedFilters();
426  QMap< QString, QgsLocatorFilter *>::const_iterator fIt = filters.constBegin();
427  for ( ; fIt != filters.constEnd(); ++fIt )
428  {
429  if ( feedback->isCanceled() )
430  return;
431 
432  if ( fIt.value() == this || !fIt.value() || !fIt.value()->enabled() )
433  continue;
434 
435  QgsLocatorResult result;
436  result.displayString = fIt.key();
437  result.description = fIt.value()->displayName();
438  result.userData = fIt.key() + ' ';
439  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
440  emit resultFetched( result );
441  }
442 }
443 
444 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
445 {
446  mLocator->search( result.userData.toString() );
447 }
448 
449 
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
Definition: qgslocator.cpp:55
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:182
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:84
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:188
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.
QgsRectangle visibleExtent() const
Return the actual extent derived from requested extent that takes takes output image size into accoun...
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:54
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
Get 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...
QMap< QString, QgsLocatorFilter * > prefixedFilters() const
Returns a map of prefix to filter, for all registered filters with valid prefixes.
Definition: qgslocator.cpp:50
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 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.