QGIS API Documentation  3.14.0-Pi (9f7028fd23)
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().boundingRect( mLineEdit->placeholderText() ).width();
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  window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes
148  if ( string.isEmpty() )
149  {
150  mLineEdit->setFocus();
151  mLineEdit->selectAll();
152  }
153  else
154  {
155  scheduleDelayedPopup();
156  mLineEdit->setFocus();
157  mLineEdit->setText( string );
158  performSearch();
159  }
160 }
161 
163 {
164  mModelBridge->invalidateResults();
165  mResultsContainer->hide();
166 }
167 
168 void QgsLocatorWidget::scheduleDelayedPopup()
169 {
170  mPopupTimer.start();
171 }
172 
173 void QgsLocatorWidget::resultAdded()
174 {
175  bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
176  if ( selectFirst )
177  {
178  int row = -1;
179  bool selectable = false;
180  while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
181  {
182  row++;
183  selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
184  }
185  if ( selectable )
186  mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
187  }
188 }
189 
190 void QgsLocatorWidget::showContextMenu( const QPoint &point )
191 {
192  QModelIndex index = mResultsView->indexAt( point );
193  if ( !index.isValid() )
194  return;
195 
196  const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index, QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
197  QMenu *contextMenu = new QMenu( mResultsView );
198  for ( auto resultAction : actions )
199  {
200  QAction *menuAction = new QAction( resultAction.text, contextMenu );
201  if ( !resultAction.iconPath.isEmpty() )
202  menuAction->setIcon( QIcon( resultAction.iconPath ) );
203  connect( menuAction, &QAction::triggered, this, [ = ]() {mModelBridge->triggerResult( index, resultAction.id );} );
204  contextMenu->addAction( menuAction );
205  }
206  contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
207 }
208 
209 void QgsLocatorWidget::performSearch()
210 {
211  mPopupTimer.stop();
212  mModelBridge->performSearch( mLineEdit->text() );
213  showList();
214 }
215 
216 void QgsLocatorWidget::showList()
217 {
218  mResultsContainer->show();
219  mResultsContainer->raise();
220 }
221 
222 void QgsLocatorWidget::triggerSearchAndShowList()
223 {
224  if ( mModelBridge->proxyModel()->rowCount() == 0 )
225  performSearch();
226  else
227  showList();
228 }
229 
230 bool QgsLocatorWidget::eventFilter( QObject *obj, QEvent *event )
231 {
232  if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
233  {
234  QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
235  switch ( keyEvent->key() )
236  {
237  case Qt::Key_Up:
238  case Qt::Key_Down:
239  case Qt::Key_PageUp:
240  case Qt::Key_PageDown:
241  triggerSearchAndShowList();
242  mHasSelectedResult = true;
243  QgsApplication::sendEvent( mResultsView, event );
244  return true;
245  case Qt::Key_Home:
246  case Qt::Key_End:
247  if ( keyEvent->modifiers() & Qt::ControlModifier )
248  {
249  triggerSearchAndShowList();
250  mHasSelectedResult = true;
251  QgsApplication::sendEvent( mResultsView, event );
252  return true;
253  }
254  break;
255  case Qt::Key_Enter:
256  case Qt::Key_Return:
257  acceptCurrentEntry();
258  return true;
259  case Qt::Key_Escape:
260  mResultsContainer->hide();
261  return true;
262  case Qt::Key_Tab:
263  mHasSelectedResult = true;
264  mResultsView->selectNextResult();
265  return true;
266  case Qt::Key_Backtab:
267  mHasSelectedResult = true;
268  mResultsView->selectPreviousResult();
269  return true;
270  default:
271  break;
272  }
273  }
274  else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
275  {
276  mHasSelectedResult = true;
277  }
278  else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
279  {
280  if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
281  {
282  mFocusTimer.stop();
283  mResultsContainer->hide();
284  }
285  }
286  else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
287  {
288  mFocusTimer.start();
289  }
290  else if ( obj == window() && event->type() == QEvent::Resize )
291  {
292  mResultsView->recalculateSize();
293  }
294  return QWidget::eventFilter( obj, event );
295 }
296 
297 void QgsLocatorWidget::configMenuAboutToShow()
298 {
299  mMenu->clear();
300  for ( QgsLocatorFilter *filter : mModelBridge->locator()->filters() )
301  {
302  if ( !filter->enabled() )
303  continue;
304 
305  QAction *action = new QAction( filter->displayName(), mMenu );
306  connect( action, &QAction::triggered, this, [ = ]
307  {
308  QString currentText = mLineEdit->text();
309  if ( currentText.isEmpty() )
310  currentText = tr( "<type here>" );
311  else
312  {
313  QStringList parts = currentText.split( ' ' );
314  if ( parts.count() > 1 && mModelBridge->locator()->filters( parts.at( 0 ) ).count() > 0 )
315  {
316  parts.pop_front();
317  currentText = parts.join( ' ' );
318  }
319  }
320 
321  mLineEdit->setText( filter->activePrefix() + ' ' + currentText );
322  mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
323  } );
324  mMenu->addAction( action );
325  }
326  mMenu->addSeparator();
327  QAction *configAction = new QAction( tr( "Configure…" ), mMenu );
328  connect( configAction, &QAction::triggered, this, &QgsLocatorWidget::configTriggered );
329  mMenu->addAction( configAction );
330 }
331 
332 
333 
334 void QgsLocatorWidget::acceptCurrentEntry()
335 {
336  if ( mModelBridge->hasQueueRequested() )
337  {
338  return;
339  }
340  else
341  {
342  if ( !mResultsView->isVisible() )
343  return;
344 
345  QModelIndex index = mResultsView->currentIndex();
346  if ( !index.isValid() )
347  return;
348 
349  mResultsContainer->hide();
350  mLineEdit->clearFocus();
351  mModelBridge->triggerResult( index );
352  }
353 }
354 
355 
356 
358 
359 //
360 // QgsLocatorResultsView
361 //
362 
363 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
364  : QTreeView( parent )
365 {
366  setRootIsDecorated( false );
367  setUniformRowHeights( true );
368  header()->hide();
369  header()->setStretchLastSection( true );
370 }
371 
372 void QgsLocatorResultsView::recalculateSize()
373 {
374  // try to show about 20 rows
375  int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
376 
377  // try to take up a sensible portion of window width (about half)
378  int width = std::max( 300, window()->size().width() / 2 );
379  QSize newSize( width, rowSize + frameWidth() * 2 );
380  // resize the floating widget this is contained within
381  parentWidget()->resize( newSize );
382  QTreeView::resize( newSize );
383 
384  header()->resizeSection( 0, width / 2 );
385  header()->resizeSection( 1, 0 );
386 }
387 
388 void QgsLocatorResultsView::selectNextResult()
389 {
390  int nextRow = currentIndex().row() + 1;
391  nextRow = nextRow % model()->rowCount( QModelIndex() );
392  setCurrentIndex( model()->index( nextRow, 0 ) );
393 }
394 
395 void QgsLocatorResultsView::selectPreviousResult()
396 {
397  int previousRow = currentIndex().row() - 1;
398  if ( previousRow < 0 )
399  previousRow = model()->rowCount( QModelIndex() ) - 1;
400  setCurrentIndex( model()->index( previousRow, 0 ) );
401 }
402 
403 //
404 // QgsLocatorFilterFilter
405 //
406 
407 QgsLocatorFilterFilter::QgsLocatorFilterFilter( QgsLocatorWidget *locator, QObject *parent )
408  : QgsLocatorFilter( parent )
409  , mLocator( locator )
410 {}
411 
412 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone() const
413 {
414  return new QgsLocatorFilterFilter( mLocator );
415 }
416 
417 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags() const
418 {
420 }
421 
422 void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocatorContext &, QgsFeedback *feedback )
423 {
424  if ( !string.isEmpty() )
425  {
426  //only shows results when nothing typed
427  return;
428  }
429 
430  for ( QgsLocatorFilter *filter : mLocator->locator()->filters() )
431  {
432  if ( feedback->isCanceled() )
433  return;
434 
435  if ( filter == this || !filter || !filter->enabled() )
436  continue;
437 
438  QgsLocatorResult result;
439  result.displayString = filter->activePrefix();
440  result.description = filter->displayName();
441  result.userData = filter->activePrefix() + ' ';
442  result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
443  emit resultFetched( result );
444  }
445 }
446 
447 void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result )
448 {
449  mLocator->search( result.userData.toString() );
450 }
451 
452 
QgsLocatorFilter
Definition: qgslocatorfilter.h:145
QgsLocatorResult::displayString
QString displayString
String displayed for result.
Definition: qgslocatorfilter.h:65
QgsLocator
Definition: qgslocator.h:57
QgsApplication::getThemeIcon
static QIcon getThemeIcon(const QString &name)
Helper to get a theme icon.
Definition: qgsapplication.cpp:605
QgsMapCanvas::destinationCrsChanged
void destinationCrsChanged()
Emitted when map CRS has changed.
qgsmapcanvas.h
QgsMapCanvas::mapSettings
const QgsMapSettings & mapSettings() const
Gets access to properties used for map rendering.
Definition: qgsmapcanvas.cpp:390
QgsLocatorWidget::eventFilter
bool eventFilter(QObject *obj, QEvent *event) override
Definition: qgslocatorwidget.cpp:230
QgsFloatingWidget::BottomLeft
@ BottomLeft
Bottom-left of widget.
Definition: qgsfloatingwidget.h:51
QgsMapCanvas
Definition: qgsmapcanvas.h:83
qgsfilterlineedit.h
QgsFilterLineEdit
Definition: qgsfilterlineedit.h:39
qgslocatormodel.h
qgslocator.h
QgsLocatorWidget
Definition: qgslocatorwidget.h:44
QgsFilterLineEdit::setShowSpinner
void setShowSpinner(bool showSpinner)
Show a spinner icon.
Definition: qgsfilterlineedit.cpp:169
QgsGuiUtils::iconSize
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window's toolbar icons.
Definition: qgsguiutils.cpp:264
QgsLocatorResult
Definition: qgslocatorfilter.h:39
qgsapplication.h
QgsLocatorModelBridge::resultAdded
void resultAdded()
Emitted when a result is added.
QgsLocatorModelBridge::proxyModel
Q_INVOKABLE QgsLocatorProxyModel * proxyModel() const
Returns the proxy model.
Definition: qgslocatormodelbridge.cpp:129
QgsLocatorModelBridge::triggerResult
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.
Definition: qgslocatormodelbridge.cpp:40
QgsLocatorContext
Definition: qgslocatorcontext.h:31
QgsLocatorModelBridge
Definition: qgslocatormodelbridge.h:42
QgsLocatorWidget::invalidateResults
void invalidateResults()
Invalidates the current search results, e.g.
Definition: qgslocatorwidget.cpp:162
QgsFeedback
Definition: qgsfeedback.h:43
QgsFloatingWidget::TopLeft
@ TopLeft
Top-left of widget.
Definition: qgsfloatingwidget.h:45
QgsMapCanvas::extentsChanged
void extentsChanged()
Emitted when the extents of the map change.
QgsLocatorModelBridge::invalidateResults
void invalidateResults()
This will invalidate current search results.
Definition: qgslocatormodelbridge.cpp:62
QgsLocatorModel::ResultActionsRole
@ ResultActionsRole
The actions to be shown for the given result in a context menu.
Definition: qgslocatormodel.h:59
qgslocatormodelbridge.h
QgsLocatorWidget::setMapCanvas
void setMapCanvas(QgsMapCanvas *canvas)
Sets a map canvas to associate with the widget.
Definition: qgslocatorwidget.cpp:123
QgsLocatorWidget::locator
QgsLocator * locator()
Returns a pointer to the locator utilized by this widget.
Definition: qgslocatorwidget.cpp:118
QgsFloatingWidget
Definition: qgsfloatingwidget.h:33
QgsLocatorModelBridge::updateCanvasCrs
void updateCanvasCrs(const QgsCoordinateReferenceSystem &crs)
Update the canvas CRS used to create search context.
Definition: qgslocatormodelbridge.cpp:73
QgsFloatingWidget::setAnchorWidget
void setAnchorWidget(QWidget *widget)
Sets the widget to "anchor" the floating widget to.
Definition: qgsfloatingwidget.cpp:36
QgsMapSettings::destinationCrs
QgsCoordinateReferenceSystem destinationCrs() const
returns CRS of destination coordinate reference system
Definition: qgsmapsettings.cpp:317
QgsFeedback::isCanceled
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:66
QgsLocatorResult::icon
QIcon icon
Icon for result.
Definition: qgslocatorfilter.h:80
QgsLocatorWidget::QgsLocatorWidget
QgsLocatorWidget(QWidget *parent SIP_TRANSFERTHIS=nullptr)
Constructor for QgsLocatorWidget.
Definition: qgslocatorwidget.cpp:31
QgsLocatorModelBridge::isRunning
bool isRunning
Definition: qgslocatormodelbridge.h:45
QgsLocatorModelBridge::updateCanvasExtent
void updateCanvasExtent(const QgsRectangle &extent)
Update the canvas extent used to create search context.
Definition: qgslocatormodelbridge.cpp:68
qgslocatorwidget.h
QgsLocatorModelBridge::hasQueueRequested
bool hasQueueRequested() const
Returns true if some text to be search is pending in the queue.
Definition: qgslocatormodelbridge.cpp:134
QgsLocatorWidget::configTriggered
void configTriggered()
Emitted when the configure option is triggered in the widget.
QgsFloatingWidget::setAnchorWidgetPoint
void setAnchorWidgetPoint(AnchorPoint point)
Returns the anchor widget's anchor point, which corresponds to the point on the anchor widget which t...
Definition: qgsfloatingwidget.cpp:76
QgsLocatorModelBridge::performSearch
Q_INVOKABLE void performSearch(const QString &text)
Perform a search.
Definition: qgslocatormodelbridge.cpp:102
QgsLocatorModelBridge::resultsCleared
void resultsCleared()
Emitted when the results are cleared.
QgsFloatingWidget::setAnchorPoint
void setAnchorPoint(AnchorPoint point)
Sets the floating widget's anchor point, which corresponds to the point on the widget which should re...
Definition: qgsfloatingwidget.cpp:66
QgsLocatorModelBridge::locator
QgsLocator * locator() const
Returns the locator.
Definition: qgslocatormodelbridge.cpp:124
QgsLocator::registerFilter
void registerFilter(QgsLocatorFilter *filter)
Registers a filter within the locator.
Definition: qgslocator.cpp:86
QgsFilterLineEdit::setShowClearButton
void setShowClearButton(bool visible)
Sets whether the widget's clear button is visible.
Definition: qgsfilterlineedit.cpp:43
qgslogger.h
qgsguiutils.h
QgsLocatorWidget::search
void search(const QString &string)
Triggers the locator widget to focus, open and start searching for a specified string.
Definition: qgslocatorwidget.cpp:145
QgsGuiUtils::scaleIconSize
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
Definition: qgsguiutils.cpp:257
QgsMapSettings::visibleExtent
QgsRectangle visibleExtent() const
Returns the actual extent derived from requested extent that takes takes output image size into accou...
Definition: qgsmapsettings.cpp:370
QgsLocatorResult::description
QString description
Descriptive text for result.
Definition: qgslocatorfilter.h:70
QgsLocatorFilter::FlagFast
@ FlagFast
Filter finds results quickly and can be safely run in the main thread.
Definition: qgslocatorfilter.h:165
QgsLocator::filters
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Definition: qgslocator.cpp:53
QgsLocatorModelBridge::isRunningChanged
void isRunningChanged()
Emitted when the running status changes.
QgsLocatorResult::userData
QVariant userData
Custom reference or other data set by the filter.
Definition: qgslocatorfilter.h:75