31 #include <QTextLayout>
37 , mLineEdit( new QgsLocatorLineEdit( this ) )
38 , mResultsView( new QgsLocatorResultsView() )
40 mLineEdit->setShowClearButton(
true );
42 mLineEdit->setPlaceholderText( tr(
"Type to locate (⌘K)" ) );
44 mLineEdit->setPlaceholderText( tr(
"Type to locate (Ctrl+K)" ) );
47 int placeholderMinWidth = mLineEdit->fontMetrics().boundingRect( mLineEdit->placeholderText() ).width();
48 int minWidth = std::max( 200,
static_cast< int >( placeholderMinWidth * 1.8 ) );
49 resize( minWidth, 30 );
50 QSizePolicy sizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
51 sizePolicy.setHorizontalStretch( 0 );
52 sizePolicy.setVerticalStretch( 0 );
53 setSizePolicy( sizePolicy );
54 setMinimumSize( QSize( minWidth, 0 ) );
56 QHBoxLayout *layout =
new QHBoxLayout();
57 layout->setContentsMargins( 0, 0, 0, 0 );
58 layout->addWidget( mLineEdit );
61 setFocusProxy( mLineEdit );
69 QHBoxLayout *containerLayout =
new QHBoxLayout();
70 containerLayout->setContentsMargins( 0, 0, 0, 0 );
71 containerLayout->addWidget( mResultsView );
72 mResultsContainer->setLayout( containerLayout );
73 mResultsContainer->hide();
75 mResultsView->setModel( mModelBridge->
proxyModel() );
76 mResultsView->setUniformRowHeights(
true );
80 mResultsView->recalculateSize();
81 mResultsView->setContextMenuPolicy( Qt::CustomContextMenu );
83 connect( mLineEdit, &QLineEdit::textChanged,
this, &QgsLocatorWidget::scheduleDelayedPopup );
84 connect( mResultsView, &QAbstractItemView::activated,
this, &QgsLocatorWidget::acceptCurrentEntry );
85 connect( mResultsView, &QAbstractItemView::customContextMenuRequested,
this, &QgsLocatorWidget::showContextMenu );
92 mPopupTimer.setInterval( 100 );
93 mPopupTimer.setSingleShot(
true );
94 connect( &mPopupTimer, &QTimer::timeout,
this, &QgsLocatorWidget::performSearch );
95 mFocusTimer.setInterval( 110 );
96 mFocusTimer.setSingleShot(
true );
97 connect( &mFocusTimer, &QTimer::timeout,
this, &QgsLocatorWidget::triggerSearchAndShowList );
99 mLineEdit->installEventFilter(
this );
100 mResultsContainer->installEventFilter(
this );
101 mResultsView->installEventFilter(
this );
102 installEventFilter(
this );
103 window()->installEventFilter(
this );
107 mMenu =
new QMenu(
this );
108 QAction *menuAction = mLineEdit->addAction(
QgsApplication::getThemeIcon( QStringLiteral(
"/search.svg" ) ), QLineEdit::LeadingPosition );
109 connect( menuAction, &QAction::triggered,
this, [ = ]
112 mResultsContainer->hide();
113 mMenu->exec( QCursor::pos() );
115 connect( mMenu, &QMenu::aboutToShow,
this, &QgsLocatorWidget::configMenuAboutToShow );
121 return mModelBridge->
locator();
126 if ( mMapCanvas == canvas )
129 for (
const QMetaObject::Connection &conn : qgis::as_const( mCanvasConnections ) )
133 mCanvasConnections.clear();
148 window()->activateWindow();
149 if (
string.isEmpty() )
151 mLineEdit->setFocus();
152 mLineEdit->selectAll();
156 scheduleDelayedPopup();
157 mLineEdit->setFocus();
158 mLineEdit->setText(
string );
166 mResultsContainer->hide();
169 void QgsLocatorWidget::scheduleDelayedPopup()
174 void QgsLocatorWidget::resultAdded()
176 bool selectFirst = !mHasSelectedResult || mModelBridge->
proxyModel()->rowCount() == 0;
180 bool selectable =
false;
181 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
184 selectable = mModelBridge->
proxyModel()->flags( mModelBridge->
proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
187 mResultsView->setCurrentIndex( mModelBridge->
proxyModel()->index( row, 0 ) );
191 void QgsLocatorWidget::showContextMenu(
const QPoint &point )
193 QModelIndex index = mResultsView->indexAt( point );
194 if ( !index.isValid() )
197 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index,
QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
198 QMenu *contextMenu =
new QMenu( mResultsView );
199 for (
auto resultAction : actions )
201 QAction *menuAction =
new QAction( resultAction.text, contextMenu );
202 if ( !resultAction.iconPath.isEmpty() )
203 menuAction->setIcon( QIcon( resultAction.iconPath ) );
204 connect( menuAction, &QAction::triggered,
this, [ = ]() {mModelBridge->
triggerResult( index, resultAction.id );} );
205 contextMenu->addAction( menuAction );
207 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
210 void QgsLocatorWidget::performSearch()
217 void QgsLocatorWidget::showList()
219 mResultsContainer->show();
220 mResultsContainer->raise();
223 void QgsLocatorWidget::triggerSearchAndShowList()
225 if ( mModelBridge->
proxyModel()->rowCount() == 0 )
233 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
235 QKeyEvent *keyEvent =
static_cast<QKeyEvent *
>( event );
236 switch ( keyEvent->key() )
241 case Qt::Key_PageDown:
242 triggerSearchAndShowList();
243 mHasSelectedResult =
true;
244 QgsApplication::sendEvent( mResultsView, event );
248 if ( keyEvent->modifiers() & Qt::ControlModifier )
250 triggerSearchAndShowList();
251 mHasSelectedResult =
true;
252 QgsApplication::sendEvent( mResultsView, event );
258 acceptCurrentEntry();
261 mResultsContainer->hide();
264 if ( !mLineEdit->performCompletion() )
266 mHasSelectedResult =
true;
267 mResultsView->selectNextResult();
270 case Qt::Key_Backtab:
271 mHasSelectedResult =
true;
272 mResultsView->selectPreviousResult();
278 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
280 mHasSelectedResult =
true;
282 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
284 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
287 mResultsContainer->hide();
290 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
294 else if ( obj == window() && event->type() == QEvent::Resize )
296 mResultsView->recalculateSize();
298 return QWidget::eventFilter( obj, event );
301 void QgsLocatorWidget::configMenuAboutToShow()
306 if ( !filter->enabled() )
309 QAction *action =
new QAction( filter->displayName(), mMenu );
310 connect( action, &QAction::triggered,
this, [ = ]
312 QString currentText = mLineEdit->text();
313 if ( currentText.isEmpty() )
314 currentText = tr(
"<type here>" );
317 QStringList parts = currentText.split(
' ' );
318 if ( parts.count() > 1 && mModelBridge->
locator()->
filters( parts.at( 0 ) ).count() > 0 )
321 currentText = parts.join(
' ' );
325 mLineEdit->setText( filter->activePrefix() +
' ' + currentText );
326 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
328 mMenu->addAction( action );
330 mMenu->addSeparator();
331 QAction *configAction =
new QAction( tr(
"Configure…" ), mMenu );
333 mMenu->addAction( configAction );
337 void QgsLocatorWidget::acceptCurrentEntry()
345 if ( !mResultsView->isVisible() )
348 QModelIndex index = mResultsView->currentIndex();
349 if ( !index.isValid() )
352 mResultsContainer->hide();
353 mLineEdit->clearFocus();
364 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
365 : QTreeView( parent )
367 setRootIsDecorated(
false );
368 setUniformRowHeights(
true );
370 header()->setStretchLastSection(
true );
373 void QgsLocatorResultsView::recalculateSize()
376 int rowSize = 20 * itemDelegate()->sizeHint( viewOptions(), model()->index( 0, 0 ) ).height();
379 int width = std::max( 300, window()->size().width() / 2 );
380 QSize newSize( width, rowSize + frameWidth() * 2 );
382 parentWidget()->resize( newSize );
383 QTreeView::resize( newSize );
385 header()->resizeSection( 0, width / 2 );
386 header()->resizeSection( 1, 0 );
389 void QgsLocatorResultsView::selectNextResult()
391 int nextRow = currentIndex().row() + 1;
392 nextRow = nextRow % model()->rowCount( QModelIndex() );
393 setCurrentIndex( model()->index( nextRow, 0 ) );
396 void QgsLocatorResultsView::selectPreviousResult()
398 int previousRow = currentIndex().row() - 1;
399 if ( previousRow < 0 )
400 previousRow = model()->rowCount( QModelIndex() ) - 1;
401 setCurrentIndex( model()->index( previousRow, 0 ) );
408 QgsLocatorFilterFilter::QgsLocatorFilterFilter(
QgsLocatorWidget *locator, QObject *parent )
410 , mLocator( locator )
413 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone()
const
415 return new QgsLocatorFilterFilter( mLocator );
418 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags()
const
425 if ( !
string.isEmpty() )
436 if ( filter ==
this || !filter || !filter->enabled() )
442 result.
userData = QString( filter->activePrefix() +
' ' );
444 emit resultFetched( result );
448 void QgsLocatorFilterFilter::triggerResult(
const QgsLocatorResult &result )
450 mLocator->search( result.
userData.toString() );
453 QgsLocatorLineEdit::QgsLocatorLineEdit(
QgsLocatorWidget *locator, QWidget *parent )
455 , mLocatorWidget( locator )
460 void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
468 QLineEdit::paintEvent( event );
473 QString currentText = text();
475 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
478 const QStringList completionList = mLocatorWidget->locator()->completionList();
480 mCompletionText.clear();
482 for (
const QString &candidate : completionList )
484 if ( candidate.startsWith( currentText ) )
486 completion = candidate.right( candidate.length() - currentText.length() );
487 mCompletionText = candidate;
492 if ( completion.isEmpty() )
497 QRect cr = cursorRect();
498 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
500 QTextLayout l( completion, font() );
502 QTextLine line = l.createLine();
503 line.setLineWidth( width() - pos.x() );
504 line.setPosition( pos );
508 p.setPen( QPen( Qt::gray, 1 ) );
509 l.draw( &p, QPoint( 0, 0 ) );
512 bool QgsLocatorLineEdit::performCompletion()
514 if ( !mCompletionText.isEmpty() )
516 setText( mCompletionText );
517 mCompletionText.clear();