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 );
127 return mModelBridge->
locator();
132 if ( mMapCanvas == canvas )
135 for (
const QMetaObject::Connection &conn : std::as_const( mCanvasConnections ) )
139 mCanvasConnections.clear();
154 window()->activateWindow();
155 if (
string.isEmpty() )
157 mLineEdit->setFocus();
158 mLineEdit->selectAll();
162 scheduleDelayedPopup();
163 mLineEdit->setFocus();
164 mLineEdit->setText(
string );
172 mResultsContainer->hide();
175 void QgsLocatorWidget::scheduleDelayedPopup()
180 void QgsLocatorWidget::resultAdded()
182 bool selectFirst = !mHasSelectedResult || mModelBridge->
proxyModel()->rowCount() == 0;
186 bool selectable =
false;
187 while ( !selectable && row < mModelBridge->proxyModel()->rowCount() )
190 selectable = mModelBridge->
proxyModel()->flags( mModelBridge->
proxyModel()->index( row, 0 ) ).testFlag( Qt::ItemIsSelectable );
193 mResultsView->setCurrentIndex( mModelBridge->
proxyModel()->index( row, 0 ) );
197 void QgsLocatorWidget::showContextMenu(
const QPoint &point )
199 QModelIndex index = mResultsView->indexAt( point );
200 if ( !index.isValid() )
203 const QList<QgsLocatorResult::ResultAction> actions = mResultsView->model()->data( index,
QgsLocatorModel::ResultActionsRole ).value<QList<QgsLocatorResult::ResultAction>>();
204 QMenu *contextMenu =
new QMenu( mResultsView );
205 for (
auto resultAction : actions )
207 QAction *menuAction =
new QAction( resultAction.text, contextMenu );
208 if ( !resultAction.iconPath.isEmpty() )
209 menuAction->setIcon( QIcon( resultAction.iconPath ) );
210 connect( menuAction, &QAction::triggered,
this, [ = ]() {mModelBridge->
triggerResult( index, resultAction.id );} );
211 contextMenu->addAction( menuAction );
213 contextMenu->exec( mResultsView->viewport()->mapToGlobal( point ) );
216 void QgsLocatorWidget::performSearch()
223 void QgsLocatorWidget::showList()
225 mResultsContainer->show();
226 mResultsContainer->raise();
229 void QgsLocatorWidget::triggerSearchAndShowList()
231 if ( mModelBridge->
proxyModel()->rowCount() == 0 )
239 if ( obj == mLineEdit && event->type() == QEvent::KeyPress )
241 QKeyEvent *keyEvent =
static_cast<QKeyEvent *
>( event );
242 switch ( keyEvent->key() )
247 case Qt::Key_PageDown:
248 triggerSearchAndShowList();
249 mHasSelectedResult =
true;
250 QgsApplication::sendEvent( mResultsView, event );
254 if ( keyEvent->modifiers() & Qt::ControlModifier )
256 triggerSearchAndShowList();
257 mHasSelectedResult =
true;
258 QgsApplication::sendEvent( mResultsView, event );
264 acceptCurrentEntry();
267 mResultsContainer->hide();
270 if ( !mLineEdit->performCompletion() )
272 mHasSelectedResult =
true;
273 mResultsView->selectNextResult();
276 case Qt::Key_Backtab:
277 mHasSelectedResult =
true;
278 mResultsView->selectPreviousResult();
284 else if ( obj == mResultsView && event->type() == QEvent::MouseButtonPress )
286 mHasSelectedResult =
true;
288 else if ( event->type() == QEvent::FocusOut && ( obj == mLineEdit || obj == mResultsContainer || obj == mResultsView ) )
290 if ( !mLineEdit->hasFocus() && !mResultsContainer->hasFocus() && !mResultsView->hasFocus() )
293 mResultsContainer->hide();
296 else if ( event->type() == QEvent::FocusIn && obj == mLineEdit )
300 else if ( obj == window() && event->type() == QEvent::Resize )
302 mResultsView->recalculateSize();
304 return QWidget::eventFilter( obj, event );
307 void QgsLocatorWidget::configMenuAboutToShow()
312 if ( !filter->enabled() )
315 QAction *action =
new QAction( filter->displayName(), mMenu );
316 connect( action, &QAction::triggered,
this, [ = ]
318 QString currentText = mLineEdit->text();
319 if ( currentText.isEmpty() )
320 currentText = tr(
"<type here>" );
323 QStringList parts = currentText.split(
' ' );
324 if ( parts.count() > 1 && mModelBridge->
locator()->
filters( parts.at( 0 ) ).count() > 0 )
327 currentText = parts.join(
' ' );
331 mLineEdit->setText( filter->activePrefix() +
' ' + currentText );
332 mLineEdit->setSelection( filter->activePrefix().length() + 1, currentText.length() );
334 mMenu->addAction( action );
336 mMenu->addSeparator();
337 QAction *configAction =
new QAction( tr(
"Configure…" ), mMenu );
339 mMenu->addAction( configAction );
343 void QgsLocatorWidget::acceptCurrentEntry()
351 if ( !mResultsView->isVisible() )
354 QModelIndex index = mResultsView->currentIndex();
355 if ( !index.isValid() )
358 mResultsContainer->hide();
359 mLineEdit->clearFocus();
370 QgsLocatorResultsView::QgsLocatorResultsView( QWidget *parent )
371 : QTreeView( parent )
373 setRootIsDecorated(
false );
374 setUniformRowHeights(
true );
376 header()->setStretchLastSection(
true );
379 void QgsLocatorResultsView::recalculateSize()
381 QStyleOptionViewItem optView;
382 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
383 optView.init(
this );
385 optView.initFrom(
this );
389 int rowSize = 20 * itemDelegate()->sizeHint( optView, model()->index( 0, 0 ) ).height();
392 int width = std::max( 300, window()->size().width() / 2 );
393 QSize newSize( width, rowSize + frameWidth() * 2 );
395 parentWidget()->resize( newSize );
396 QTreeView::resize( newSize );
398 header()->resizeSection( 0, width / 2 );
399 header()->resizeSection( 1, 0 );
402 void QgsLocatorResultsView::selectNextResult()
404 const int rowCount = model()->rowCount( QModelIndex() );
408 int nextRow = currentIndex().row() + 1;
409 nextRow = nextRow % rowCount;
410 setCurrentIndex( model()->index( nextRow, 0 ) );
413 void QgsLocatorResultsView::selectPreviousResult()
415 const int rowCount = model()->rowCount( QModelIndex() );
419 int previousRow = currentIndex().row() - 1;
420 if ( previousRow < 0 )
421 previousRow = rowCount - 1;
422 setCurrentIndex( model()->index( previousRow, 0 ) );
429 QgsLocatorFilterFilter::QgsLocatorFilterFilter(
QgsLocatorWidget *locator, QObject *parent )
431 , mLocator( locator )
434 QgsLocatorFilterFilter *QgsLocatorFilterFilter::clone()
const
436 return new QgsLocatorFilterFilter( mLocator );
439 QgsLocatorFilter::Flags QgsLocatorFilterFilter::flags()
const
446 if ( !
string.isEmpty() )
457 if ( filter ==
this || !filter || !filter->enabled() )
463 result.
userData = QString( filter->activePrefix() +
' ' );
465 emit resultFetched( result );
469 void QgsLocatorFilterFilter::triggerResult(
const QgsLocatorResult &result )
471 mLocator->search( result.
userData.toString() );
474 QgsLocatorLineEdit::QgsLocatorLineEdit(
QgsLocatorWidget *locator, QWidget *parent )
476 , mLocatorWidget( locator )
481 void QgsLocatorLineEdit::paintEvent( QPaintEvent *event )
489 QLineEdit::paintEvent( event );
494 QString currentText = text();
496 if ( currentText.length() == 0 || cursorPosition() < currentText.length() )
499 const QStringList completionList = mLocatorWidget->locator()->completionList();
501 mCompletionText.clear();
503 for (
const QString &candidate : completionList )
505 if ( candidate.startsWith( currentText ) )
507 completion = candidate.right( candidate.length() - currentText.length() );
508 mCompletionText = candidate;
513 if ( completion.isEmpty() )
518 QRect cr = cursorRect();
519 QPoint pos = cr.topRight() - QPoint( cr.width() / 2, 0 );
521 QTextLayout l( completion, font() );
523 QTextLine line = l.createLine();
524 line.setLineWidth( width() - pos.x() );
525 line.setPosition( pos );
529 p.setPen( QPen( Qt::gray, 1 ) );
530 l.draw( &p, QPoint( 0, 0 ) );
533 bool QgsLocatorLineEdit::performCompletion()
535 if ( !mCompletionText.isEmpty() )
537 setText( mCompletionText );
538 mCompletionText.clear();