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