36#include "moc_qgslocatorwidget.cpp"
38using namespace Qt::StringLiterals;
47 , mLineEdit( new QgsLocatorLineEdit( this ) )
48 , mResultsView( new QgsLocatorResultsView() )
50 setObjectName( u
"LocatorWidget"_s );
51 mLineEdit->setShowClearButton(
true );
53 mLineEdit->setPlaceholderText( tr(
"Type to locate (⌘K)" ) );
55 mLineEdit->setPlaceholderText( tr(
"Type to locate (Ctrl+K)" ) );
58 int placeholderMinWidth = mLineEdit->fontMetrics().boundingRect( mLineEdit->placeholderText() ).width();
59 int minWidth = std::max( 200,
static_cast<int>( placeholderMinWidth * 1.8 ) );
60 resize( minWidth, 30 );
61 QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
62 sizePolicy.setHorizontalStretch( 0 );
63 sizePolicy.setVerticalStretch( 0 );
64 setSizePolicy( sizePolicy );
65 setMinimumSize( QSize( minWidth, 0 ) );
67 QHBoxLayout *layout =
new QHBoxLayout();
68 layout->setContentsMargins( 0, 0, 0, 0 );
69 layout->addWidget( mLineEdit );
72 setFocusProxy( mLineEdit );
76 mResultsContainer->setAnchorWidget( mLineEdit );
80 QHBoxLayout *containerLayout =
new QHBoxLayout();
81 containerLayout->setContentsMargins( 0, 0, 0, 0 );
82 containerLayout->addWidget( mResultsView );
83 mResultsContainer->setLayout( containerLayout );
84 mResultsContainer->hide();
86 mResultsView->setModel( mModelBridge->proxyModel() );
87 mResultsView->setUniformRowHeights(
true );
90 mResultsView->setIconSize( QSize( iconSize, iconSize ) );
91 mResultsView->recalculateSize();
92 mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
94 connect( mLineEdit, &QLineEdit::textChanged,
this, &QgsLocatorWidget::scheduleDelayedPopup );
95 connect( mResultsView, &QAbstractItemView::activated,
this, &QgsLocatorWidget::acceptCurrentEntry );
96 connect( mResultsView->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &QgsLocatorWidget::selectionChanged );
97 connect( mResultsView, &QAbstractItemView::customContextMenuRequested,
this, &QgsLocatorWidget::showContextMenu );
104 mPopupTimer.setInterval( 100 );
105 mPopupTimer.setSingleShot(
true );
106 connect( &mPopupTimer, &QTimer::timeout,
this, &QgsLocatorWidget::performSearch );
107 mFocusTimer.setInterval( 110 );
108 mFocusTimer.setSingleShot(
true );
109 connect( &mFocusTimer, &QTimer::timeout,
this, &QgsLocatorWidget::triggerSearchAndShowList );
111 mLineEdit->installEventFilter(
this );
112 mResultsContainer->installEventFilter(
this );
113 mResultsView->installEventFilter(
this );
114 installEventFilter(
this );
115 window()->installEventFilter(
this );
117 mModelBridge->locator()->registerFilter(
new QgsLocatorFilterFilter(
this,
this ) );
119 mMenu =
new QMenu(
this );
121 connect( menuAction, &QAction::triggered,
this, [
this] {
123 mResultsContainer->hide();
124 mMenu->exec( QCursor::pos() );
126 connect( mMenu, &QMenu::aboutToShow,
this, &QgsLocatorWidget::configMenuAboutToShow );
136 return mModelBridge->locator();
141 if ( mMapCanvas == canvas )
144 for (
const QMetaObject::Connection &conn : std::as_const( mCanvasConnections ) )
148 mCanvasConnections.clear();
153 mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() );
154 mModelBridge->updateCanvasCrs( mMapCanvas->mapSettings().destinationCrs() );
156 << connect( mMapCanvas, &
QgsMapCanvas::extentsChanged,
this, [
this]() { mModelBridge->updateCanvasExtent( mMapCanvas->mapSettings().visibleExtent() ); } )
163 mLineEdit->setPlaceholderText( text );
168 mResultsContainer->setAnchorPoint( anchorPoint );
169 mResultsContainer->setAnchorWidgetPoint( anchorWidgetPoint );
174 window()->activateWindow();
175 if (
string.isEmpty() )
177 mLineEdit->setFocus();
178 mLineEdit->selectAll();
182 scheduleDelayedPopup();
183 mLineEdit->setFocus();
184 mLineEdit->setText(
string );
191 mModelBridge->invalidateResults();
192 mResultsContainer->hide();
195void QgsLocatorWidget::scheduleDelayedPopup()
200void QgsLocatorWidget::resultAdded()
202 bool selectFirst = !mHasSelectedResult || mModelBridge->proxyModel()->rowCount() == 0;
206 bool selectable =
false;
207 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
210 selectable = mModelBridge->proxyModel()->flags( mModelBridge->proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
213 mResultsView->setCurrentIndex( mModelBridge->proxyModel()->index( row, 0 ) );
217void QgsLocatorWidget::showContextMenu(
const QPoint &point )
219 QModelIndex index = mResultsView->indexAt( point );
220 if ( !index.isValid() )
224 QMenu *contextMenu =
new QMenu( mResultsView );
225 for (
auto resultAction : actions )
227 QAction *menuAction =
new QAction( resultAction.text, contextMenu );
228 if ( !resultAction.iconPath.isEmpty() )
229 menuAction->setIcon( QIcon( resultAction.iconPath ) );
230 connect( menuAction, &QAction::triggered,
this, [
this, index, resultAction]() { mModelBridge->triggerResult( index, resultAction.id ); } );
231 contextMenu->addAction( menuAction );
233 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
236void QgsLocatorWidget::performSearch()
239 mModelBridge->performSearch( mLineEdit->text() );
243void QgsLocatorWidget::showList()
245 mResultsContainer->show();
246 mResultsContainer->raise();
249void QgsLocatorWidget::triggerSearchAndShowList()
251 if ( mModelBridge->proxyModel()->rowCount() == 0 )
259 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
261 QKeyEvent *keyEvent =
static_cast<QKeyEvent *
>( event );
262 switch ( keyEvent->key() )
267 case Qt::Key_PageDown:
268 triggerSearchAndShowList();
269 mHasSelectedResult =
true;
270 QgsApplication::sendEvent( mResultsView, event );
274 if ( keyEvent->modifiers() & Qt::ControlModifier )
276 triggerSearchAndShowList();
277 mHasSelectedResult =
true;
278 QgsApplication::sendEvent( mResultsView, event );
284 acceptCurrentEntry();
287 mResultsContainer->hide();
290 if ( !mLineEdit->performCompletion() )
292 mHasSelectedResult =
true;
293 mResultsView->selectNextResult();
296 case Qt::Key_Backtab:
297 mHasSelectedResult =
true;
298 mResultsView->selectPreviousResult();
304 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
306 mHasSelectedResult =
true;
308 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
310 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
313 mResultsContainer->hide();
316 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
320 else if ( obj == window() && event->type() == QEvent::Resize )
322 mResultsView->recalculateSize();
324 return QWidget::eventFilter( obj, event );
327void QgsLocatorWidget::configMenuAboutToShow()
332 if ( !filter->enabled() )
335 QAction *action =
new QAction( filter->displayName(), mMenu );
336 connect( action, &QAction::triggered,
this, [
this, filter] {
337 QString currentText = mLineEdit->text();
338 if ( currentText.isEmpty() )
339 currentText = tr(
"<type here>" );
342 QStringList parts = currentText.split(
' ' );
343 if ( parts.count() > 1 && mModelBridge->
locator()->
filters( parts.at( 0 ) ).count() > 0 )
346 currentText = parts.join(
' ' );
350 mLineEdit->setText( filter->activePrefix() +
' ' + currentText );
351 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
353 mMenu->addAction( action );
355 mMenu->addSeparator();
356 QAction *configAction =
new QAction( tr(
"Configure…" ), mMenu );
358 mMenu->addAction( configAction );
362void QgsLocatorWidget::acceptCurrentEntry()
364 if ( mModelBridge->hasQueueRequested() )
370 if ( !mResultsView->isVisible() )
373 QModelIndex index = mResultsView->currentIndex();
374 if ( !index.isValid() )
377 mResultsContainer->hide();
378 mLineEdit->clearFocus();
379 mModelBridge->triggerResult( index );
383void QgsLocatorWidget::selectionChanged(
const QItemSelection &selected,
const QItemSelection &deselected )
385 if ( !mResultsView->isVisible() )
388 mModelBridge->selectionChanged( selected, deselected );
397QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
398 : QTreeView( parent )
400 setRootIsDecorated(
false );
401 setUniformRowHeights(
true );
403 header()->setStretchLastSection(
true );
406void QgsLocatorResultsView::recalculateSize()
408 QStyleOptionViewItem optView;
409 optView.initFrom(
this );
415 int width = std::max( 300, window()->size().width() / 2 );
416 QSize newSize( width, rowSize + frameWidth() * 2 );
418 parentWidget()->resize( newSize );
419 QTreeView::resize( newSize );
421 header()->resizeSection( 0, width / 2 );
422 header()->resizeSection( 1, 0 );
425void QgsLocatorResultsView::selectNextResult()
427 const int rowCount = model()->rowCount( QModelIndex() );
431 int nextRow = currentIndex().row() + 1;
432 nextRow = nextRow % rowCount;
433 setCurrentIndex( model()->index( nextRow, 0 ) );
436void QgsLocatorResultsView::selectPreviousResult()
438 const int rowCount = model()->rowCount( QModelIndex() );
442 int previousRow = currentIndex().row() - 1;
443 if ( previousRow < 0 )
444 previousRow = rowCount - 1;
445 setCurrentIndex( model()->index( previousRow, 0 ) );
452QgsLocatorFilterFilter::QgsLocatorFilterFilter(
QgsLocatorWidget *locator, QObject *parent )
454 , mLocator( locator )
457QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone()
const
459 return new QgsLocatorFilterFilter( mLocator );
469 if ( !
string.isEmpty() )
480 if ( filter ==
this || !filter || !filter->enabled() )
486 result.
setUserData( QString( filter->activePrefix() +
' ' ) );
488 emit resultFetched( result );
494 mLocator->search( result.
userData().toString() );
497QgsLocatorLineEdit::QgsLocatorLineEdit(
QgsLocatorWidget *locator, QWidget *parent )
499 , mLocatorWidget( locator )
504void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
512 QLineEdit::paintEvent( event );
517 QString currentText = text();
519 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
522 const QStringList completionList = mLocatorWidget->locator()->completionList();
524 mCompletionText.clear();
526 for (
const QString &candidate : completionList )
528 if ( candidate.startsWith( currentText ) )
530 completion = candidate.right( candidate.length() - currentText.length() );
531 mCompletionText = candidate;
536 if ( completion.isEmpty() )
541 QRect cr = cursorRect();
542 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
544 QTextLayout l( completion, font() );
546 QTextLine line = l.createLine();
547 line.setLineWidth( width() - pos.x() );
548 line.setPosition( pos );
552 p.setPen( QPen( Qt::gray, 1 ) );
553 l.draw( &p, QPoint( 0, 0 ) );
556bool QgsLocatorLineEdit::performCompletion()
558 if ( !mCompletionText.isEmpty() )
560 setText( mCompletionText );
561 mCompletionText.clear();
QFlags< SettingsOption > SettingsOptions
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
bool isCanceled() const
Tells whether the operation has been canceled already.
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
Encapsulates the properties relating to the context of a locator search.
Abstract base class for filters which collect locator results.
@ FlagFast
Filter finds results quickly and can be safely run in the main thread.
Provides the core functionality to be used in a locator widget.
void isRunningChanged()
Emitted when the running status changes.
void resultAdded()
Emitted when a result is added.
QgsLocator * locator() const
Returns the locator.
void resultsCleared()
Emitted when the results are cleared.
@ ResultActions
The actions to be shown for the given result in a context menu.
Encapsulates properties of an individual matching result found by a QgsLocatorFilter.
QString description
Descriptive text for result.
void setUserData(const QVariant &userData)
Set userData for the locator result.
QString displayString
String displayed for result.
QIcon icon
Icon for result.
Handles the management of QgsLocatorFilter objects and async collection of search results from them.
void searchPrepared()
Emitted when locator has prepared the search (.
QList< QgsLocatorFilter * > filters(const QString &prefix=QString())
Returns the list of filters registered in the locator.
Map canvas is a class for displaying all GIS data types on a canvas.
void extentsChanged()
Emitted when the extents of the map change.
void destinationCrsChanged()
Emitted when map CRS has changed.
static QgsProject * instance()
Returns the QgsProject singleton instance.
void transformContextChanged()
Emitted when the project transformContext() is changed.
An integer settings entry.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...