File indexing completed on 2024-05-05 04:40:55
0001 /* 0002 SPDX-FileCopyrightText: 2007 David Nolden <david.nolden.kdevelop@art-master.de> 0003 SPDX-FileCopyrightText: 2016 Kevin Funk <kfunk@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "quickopenwidget.h" 0009 #include "debug.h" 0010 0011 #include "expandingtree/expandingdelegate.h" 0012 #include "quickopenmodel.h" 0013 0014 #include <icore.h> 0015 #include <iuicontroller.h> 0016 0017 #include <QDialog> 0018 #include <QSortFilterProxyModel> 0019 #include <QIdentityProxyModel> 0020 #include <QMenuBar> 0021 #include <QKeyEvent> 0022 #include <QScrollBar> 0023 0024 #include <KParts/MainWindow> 0025 #include <KTextEditor/CodeCompletionModel> 0026 0027 using namespace KDevelop; 0028 0029 class QuickOpenDelegate 0030 : public ExpandingDelegate 0031 { 0032 Q_OBJECT 0033 public: 0034 explicit QuickOpenDelegate(ExpandingWidgetModel* model, QObject* parent = nullptr) : ExpandingDelegate(model, parent) 0035 { 0036 } 0037 QVector<QTextLayout::FormatRange> createHighlighting(const QModelIndex& index, QStyleOptionViewItem& option) const override 0038 { 0039 QList<QVariant> highlighting = index.data(KTextEditor::CodeCompletionModel::CustomHighlight).toList(); 0040 if (!highlighting.isEmpty()) { 0041 return highlightingFromVariantList(highlighting); 0042 } 0043 return ExpandingDelegate::createHighlighting(index, option); 0044 } 0045 }; 0046 0047 QuickOpenWidget::QuickOpenWidget(QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly, bool noSearchField) 0048 : m_model(model) 0049 , m_expandedTemporary(false) 0050 , m_hadNoCommandSinceAlt(true) 0051 { 0052 m_filterTimer.setSingleShot(true); 0053 connect(&m_filterTimer, &QTimer::timeout, this, &QuickOpenWidget::applyFilter); 0054 0055 ui.setupUi(this); 0056 ui.list->header()->hide(); 0057 ui.list->setRootIsDecorated(false); 0058 ui.list->setVerticalScrollMode(QAbstractItemView::ScrollPerItem); 0059 0060 connect(ui.list->verticalScrollBar(), &QScrollBar::valueChanged, m_model, &QuickOpenModel::placeExpandingWidgets); 0061 0062 ui.searchLine->setFocus(); 0063 0064 ui.list->setItemDelegate(new QuickOpenDelegate(m_model, ui.list)); 0065 0066 if (!listOnly) { 0067 const QStringList allTypes = m_model->allTypes(); 0068 const QStringList allScopes = m_model->allScopes(); 0069 0070 auto* itemsMenu = new QMenu(this); 0071 0072 for (const QString& type : allTypes) { 0073 auto* action = new QAction(type, itemsMenu); 0074 action->setCheckable(true); 0075 action->setChecked(initialItems.isEmpty() || initialItems.contains(type)); 0076 connect(action, &QAction::toggled, this, &QuickOpenWidget::updateProviders, Qt::QueuedConnection); 0077 itemsMenu->addAction(action); 0078 } 0079 0080 ui.itemsButton->setMenu(itemsMenu); 0081 0082 auto* scopesMenu = new QMenu(this); 0083 0084 for (const QString& scope : allScopes) { 0085 auto* action = new QAction(scope, scopesMenu); 0086 action->setCheckable(true); 0087 action->setChecked(initialScopes.isEmpty() || initialScopes.contains(scope)); 0088 0089 connect(action, &QAction::toggled, this, &QuickOpenWidget::updateProviders, Qt::QueuedConnection); 0090 scopesMenu->addAction(action); 0091 } 0092 0093 ui.scopesButton->setMenu(scopesMenu); 0094 } else { 0095 ui.list->setFocusPolicy(Qt::StrongFocus); 0096 ui.scopesButton->hide(); 0097 ui.itemsButton->hide(); 0098 ui.label->hide(); 0099 ui.label_2->hide(); 0100 } 0101 0102 showSearchField(!noSearchField); 0103 0104 ui.okButton->hide(); 0105 ui.cancelButton->hide(); 0106 0107 ui.searchLine->installEventFilter(this); 0108 ui.list->installEventFilter(this); 0109 ui.list->setFocusPolicy(Qt::NoFocus); 0110 ui.scopesButton->setFocusPolicy(Qt::NoFocus); 0111 ui.itemsButton->setFocusPolicy(Qt::NoFocus); 0112 0113 connect(ui.searchLine, &QLineEdit::textChanged, this, &QuickOpenWidget::textChanged); 0114 0115 connect(ui.list, &ExpandingTree::doubleClicked, this, &QuickOpenWidget::doubleClicked); 0116 0117 connect(ui.okButton, &QPushButton::clicked, this, &QuickOpenWidget::accept); 0118 connect(ui.okButton, &QPushButton::clicked, this, &QuickOpenWidget::ready); 0119 connect(ui.cancelButton, &QPushButton::clicked, this, &QuickOpenWidget::ready); 0120 0121 updateProviders(); 0122 updateTimerInterval(true); 0123 0124 // no need to call this, it's done by updateProviders already 0125 // m_model->restart(); 0126 } 0127 0128 void QuickOpenWidget::showStandardButtons(bool show) 0129 { 0130 if (show) { 0131 ui.okButton->show(); 0132 ui.cancelButton->show(); 0133 } else { 0134 ui.okButton->hide(); 0135 ui.cancelButton->hide(); 0136 } 0137 } 0138 0139 bool QuickOpenWidget::sortingEnabled() const 0140 { 0141 return m_sortingEnabled; 0142 } 0143 0144 void QuickOpenWidget::setSortingEnabled(bool enabled) 0145 { 0146 m_sortingEnabled = enabled; 0147 } 0148 0149 void QuickOpenWidget::updateTimerInterval(bool cheapFilterChange) 0150 { 0151 const int MAX_ITEMS = 10000; 0152 if (cheapFilterChange && m_model->rowCount(QModelIndex()) < MAX_ITEMS) { 0153 // cheap change and there are currently just a few items, 0154 // so apply filter instantly 0155 m_filterTimer.setInterval(0); 0156 } else if (m_model->unfilteredRowCount() < MAX_ITEMS) { 0157 // not a cheap change, but there are generally 0158 // just a few items in the list: apply filter instantly 0159 m_filterTimer.setInterval(0); 0160 } else { 0161 // otherwise use a timer to prevent sluggishness while typing 0162 m_filterTimer.setInterval(300); 0163 } 0164 } 0165 0166 void QuickOpenWidget::showEvent(QShowEvent* e) 0167 { 0168 QWidget::showEvent(e); 0169 0170 // The column width only has an effect _after_ the widget has been shown 0171 ui.list->setColumnWidth(0, 20); 0172 } 0173 0174 void QuickOpenWidget::setAlternativeSearchField(QLineEdit* alterantiveSearchField) 0175 { 0176 ui.searchLine = alterantiveSearchField; 0177 ui.searchLine->installEventFilter(this); 0178 connect(ui.searchLine, &QLineEdit::textChanged, this, &QuickOpenWidget::textChanged); 0179 } 0180 0181 void QuickOpenWidget::showSearchField(bool b) 0182 { 0183 if (b) { 0184 ui.searchLine->show(); 0185 ui.searchLabel->show(); 0186 } else { 0187 ui.searchLine->hide(); 0188 ui.searchLabel->hide(); 0189 } 0190 } 0191 0192 void QuickOpenWidget::prepareShow() 0193 { 0194 ui.list->setModel(nullptr); 0195 ui.list->setVerticalScrollMode(QAbstractItemView::ScrollPerItem); 0196 m_model->setTreeView(ui.list); 0197 0198 // set up proxy filter 0199 delete m_proxy; 0200 m_proxy = nullptr; 0201 0202 if (sortingEnabled()) { 0203 auto sortFilterProxyModel = new QSortFilterProxyModel(this); 0204 sortFilterProxyModel->setDynamicSortFilter(true); 0205 m_proxy = sortFilterProxyModel; 0206 } else { 0207 m_proxy = new QIdentityProxyModel(this); 0208 } 0209 m_proxy->setSourceModel(m_model); 0210 if (sortingEnabled()) { 0211 m_proxy->sort(1); 0212 } 0213 ui.list->setModel(m_proxy); 0214 0215 m_filterTimer.stop(); 0216 m_filter = QString(); 0217 0218 if (!m_preselectedText.isEmpty()) { 0219 ui.searchLine->setText(m_preselectedText); 0220 ui.searchLine->selectAll(); 0221 } 0222 0223 m_model->restart(false); 0224 0225 connect(ui.list->selectionModel(), &QItemSelectionModel::currentRowChanged, 0226 this, &QuickOpenWidget::callRowSelected); 0227 connect(ui.list->selectionModel(), &QItemSelectionModel::selectionChanged, 0228 this, &QuickOpenWidget::callRowSelected); 0229 } 0230 0231 void QuickOpenWidgetDialog::run() 0232 { 0233 m_widget->prepareShow(); 0234 m_dialog->show(); 0235 } 0236 0237 QuickOpenWidget::~QuickOpenWidget() 0238 { 0239 m_model->setTreeView(nullptr); 0240 } 0241 0242 QuickOpenWidgetDialog::QuickOpenWidgetDialog(const QString& title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly, bool noSearchField) 0243 { 0244 m_widget = new QuickOpenWidget(model, initialItems, initialScopes, listOnly, noSearchField); 0245 // the QMenu might close on esc and we want to close the whole dialog then 0246 connect(m_widget, &QuickOpenWidget::aboutToHide, this, &QuickOpenWidgetDialog::deleteLater); 0247 0248 m_dialog = new QDialog(ICore::self()->uiController()->activeMainWindow()); 0249 m_dialog->resize(QSize(800, 400)); 0250 0251 m_dialog->setWindowTitle(title); 0252 auto* layout = new QVBoxLayout(m_dialog); 0253 layout->addWidget(m_widget); 0254 m_widget->showStandardButtons(true); 0255 connect(m_widget, &QuickOpenWidget::ready, m_dialog, &QDialog::close); 0256 connect(m_dialog, &QDialog::accepted, m_widget, &QuickOpenWidget::accept); 0257 } 0258 0259 QuickOpenWidgetDialog::~QuickOpenWidgetDialog() 0260 { 0261 delete m_dialog; 0262 } 0263 0264 void QuickOpenWidget::setPreselectedText(const QString& text) 0265 { 0266 m_preselectedText = text; 0267 } 0268 0269 void QuickOpenWidget::updateProviders() 0270 { 0271 if (QAction* action = (sender() ? qobject_cast<QAction*>(sender()) : nullptr)) { 0272 auto* menu = qobject_cast<QMenu*>(action->parentWidget()); 0273 if (menu) { 0274 menu->show(); 0275 menu->setActiveAction(action); 0276 } 0277 } 0278 0279 QStringList checkedItems; 0280 0281 if (ui.itemsButton->menu()) { 0282 for (QObject* obj : ui.itemsButton->menu()->children()) { 0283 auto* box = qobject_cast<QAction*>(obj); 0284 if (box) { 0285 if (box->isChecked()) { 0286 checkedItems << box->text().remove(QLatin1Char('&')); 0287 } 0288 } 0289 } 0290 0291 ui.itemsButton->setText(checkedItems.join(QLatin1String(", "))); 0292 } 0293 0294 QStringList checkedScopes; 0295 0296 if (ui.scopesButton->menu()) { 0297 for (QObject* obj : ui.scopesButton->menu()->children()) { 0298 auto* box = qobject_cast<QAction*>(obj); 0299 if (box) { 0300 if (box->isChecked()) { 0301 checkedScopes << box->text().remove(QLatin1Char('&')); 0302 } 0303 } 0304 } 0305 0306 ui.scopesButton->setText(checkedScopes.join(QLatin1String(", "))); 0307 } 0308 0309 emit itemsChanged(checkedItems); 0310 emit scopesChanged(checkedScopes); 0311 m_model->enableProviders(checkedItems, checkedScopes); 0312 } 0313 0314 void QuickOpenWidget::textChanged(const QString& str) 0315 { 0316 QString strTrimmed = str.trimmed(); 0317 0318 // "cheap" when something was just appended to the current filter 0319 updateTimerInterval(strTrimmed.startsWith(m_filter)); 0320 m_filter = strTrimmed; 0321 m_filterTimer.start(); 0322 } 0323 0324 void QuickOpenWidget::applyFilter() 0325 { 0326 m_model->textChanged(m_filter); 0327 0328 QModelIndex currentIndex = m_model->index(0, 0, QModelIndex()); 0329 0330 ui.list->selectionModel()->setCurrentIndex(m_proxy->mapFromSource(currentIndex), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current); 0331 0332 callRowSelected(); 0333 } 0334 0335 void QuickOpenWidget::callRowSelected() 0336 { 0337 const QModelIndex currentIndex = ui.list->currentIndex(); 0338 if (currentIndex.isValid()) { 0339 m_model->rowSelected(m_proxy->mapToSource(currentIndex)); 0340 } else { 0341 qCDebug(PLUGIN_QUICKOPEN) << "current index is not valid"; 0342 } 0343 } 0344 0345 void QuickOpenWidget::accept() 0346 { 0347 QString filterText = ui.searchLine->text(); 0348 m_model->execute(m_proxy->mapToSource(ui.list->currentIndex()), filterText); 0349 } 0350 0351 void QuickOpenWidget::doubleClicked(const QModelIndex& index) 0352 { 0353 // crash guard: https://bugs.kde.org/show_bug.cgi?id=297178 0354 ui.list->setCurrentIndex(index); 0355 QMetaObject::invokeMethod(this, "accept", Qt::QueuedConnection); 0356 QMetaObject::invokeMethod(this, "ready", Qt::QueuedConnection); 0357 } 0358 0359 void QuickOpenWidget::avoidMenuAltFocus() 0360 { 0361 // send an invalid key event to the main menu bar. The menu bar will 0362 // stop listening when observing another key than ALT between the press 0363 // and the release. 0364 QKeyEvent event1(QEvent::KeyPress, 0, Qt::NoModifier); 0365 QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event1); 0366 QKeyEvent event2(QEvent::KeyRelease, 0, Qt::NoModifier); 0367 QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event2); 0368 } 0369 0370 bool QuickOpenWidget::eventFilter(QObject* watched, QEvent* event) 0371 { 0372 auto getInterface = [this]() { 0373 const QModelIndex index = m_proxy->mapToSource(ui.list->currentIndex()); 0374 QWidget* widget = m_model->expandingWidget(index); 0375 return dynamic_cast<KDevelop::QuickOpenEmbeddedWidgetInterface*>(widget); 0376 }; 0377 0378 auto* keyEvent = dynamic_cast<QKeyEvent*>(event); 0379 0380 if (event->type() == QEvent::KeyRelease) { 0381 if (keyEvent->key() == Qt::Key_Alt) { 0382 if ((m_expandedTemporary && m_altDownTime.msecsTo(QTime::currentTime()) > 300) || (!m_expandedTemporary && m_altDownTime.msecsTo(QTime::currentTime()) < 300 && m_hadNoCommandSinceAlt)) { 0383 //Unexpand the item 0384 QModelIndex row = m_proxy->mapToSource(ui.list->selectionModel()->currentIndex()); 0385 if (row.isValid()) { 0386 row = row.sibling(row.row(), 0); 0387 if (m_model->isExpanded(row)) { 0388 m_model->setExpanded(row, false); 0389 } 0390 } 0391 } 0392 m_expandedTemporary = false; 0393 } 0394 } 0395 0396 if (event->type() == QEvent::KeyPress) { 0397 m_hadNoCommandSinceAlt = false; 0398 if (keyEvent->key() == Qt::Key_Alt) { 0399 avoidMenuAltFocus(); 0400 m_hadNoCommandSinceAlt = true; 0401 //Expand 0402 QModelIndex row = m_proxy->mapToSource(ui.list->selectionModel()->currentIndex()); 0403 if (row.isValid()) { 0404 row = row.sibling(row.row(), 0); 0405 m_altDownTime = QTime::currentTime(); 0406 if (!m_model->isExpanded(row)) { 0407 m_expandedTemporary = true; 0408 m_model->setExpanded(row, true); 0409 } 0410 } 0411 } 0412 0413 switch (keyEvent->key()) { 0414 case Qt::Key_Tab: 0415 if (keyEvent->modifiers() == Qt::NoModifier) { 0416 // Tab should work just like Down 0417 QKeyEvent keyDownPress(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); 0418 QCoreApplication::sendEvent(ui.list, &keyDownPress); 0419 QKeyEvent keyDownRelease(QEvent::KeyRelease, Qt::Key_Down, Qt::NoModifier); 0420 QCoreApplication::sendEvent(ui.list, &keyDownRelease); 0421 return true; // eat event 0422 } 0423 break; 0424 case Qt::Key_Backtab: 0425 if (keyEvent->modifiers() == Qt::ShiftModifier) { 0426 // Shift + Tab should work just like Up 0427 QKeyEvent keyUpPress(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); 0428 QCoreApplication::sendEvent(ui.list, &keyUpPress); 0429 QKeyEvent keyUpRelease(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); 0430 QCoreApplication::sendEvent(ui.list, &keyUpRelease); 0431 return true; // eat event 0432 } 0433 break; 0434 case Qt::Key_Backspace: 0435 if (keyEvent->modifiers() == Qt::AltModifier) { 0436 if (auto interface = getInterface()) { 0437 interface->back(); 0438 return true; // eat event 0439 } 0440 } 0441 break; 0442 case Qt::Key_Down: 0443 case Qt::Key_Up: 0444 if (keyEvent->modifiers() == Qt::AltModifier) { 0445 if (auto interface = getInterface()) { 0446 if (keyEvent->key() == Qt::Key_Down) { 0447 interface->down(); 0448 } else { 0449 interface->up(); 0450 } 0451 return true; // eat event 0452 } 0453 break; 0454 } 0455 [[fallthrough]]; 0456 case Qt::Key_PageUp: 0457 case Qt::Key_PageDown: 0458 if (watched == ui.list) { 0459 break; 0460 } 0461 QApplication::sendEvent(ui.list, event); 0462 //callRowSelected(); 0463 return true; // eat event 0464 0465 case Qt::Key_Left: { 0466 //Expand/unexpand 0467 if (keyEvent->modifiers() == Qt::AltModifier) { 0468 //Eventually Send action to the widget 0469 if (auto interface = getInterface()) { 0470 interface->previous(); 0471 return true; // eat event 0472 } 0473 } else { 0474 QModelIndex row = m_proxy->mapToSource(ui.list->currentIndex()); 0475 if (row.isValid()) { 0476 row = row.sibling(row.row(), 0); 0477 0478 if (m_model->isExpanded(row)) { 0479 m_model->setExpanded(row, false); 0480 return true; // eat event 0481 } 0482 } 0483 } 0484 break; 0485 } 0486 case Qt::Key_Right: { 0487 //Expand/unexpand 0488 if (keyEvent->modifiers() == Qt::AltModifier) { 0489 //Eventually Send action to the widget 0490 if (auto interface = getInterface()) { 0491 interface->next(); 0492 return true; // eat event 0493 } 0494 } else { 0495 QModelIndex row = m_proxy->mapToSource(ui.list->selectionModel()->currentIndex()); 0496 if (row.isValid()) { 0497 row = row.sibling(row.row(), 0); 0498 0499 if (!m_model->isExpanded(row)) { 0500 m_model->setExpanded(row, true); 0501 return true; // eat event 0502 } 0503 } 0504 } 0505 break; 0506 } 0507 case Qt::Key_Return: 0508 case Qt::Key_Enter: { 0509 if (m_filterTimer.isActive()) { 0510 m_filterTimer.stop(); 0511 applyFilter(); 0512 } 0513 if (keyEvent->modifiers() == Qt::AltModifier) { 0514 //Eventually Send action to the widget 0515 if (auto interface = getInterface()) { 0516 interface->accept(); 0517 return true; // eat event 0518 } 0519 } else { 0520 QString filterText = ui.searchLine->text(); 0521 0522 //Safety: Track whether this object is deleted. When execute() is called, a dialog may be opened, 0523 //which kills the quickopen widget. 0524 QPointer<QObject> stillExists(this); 0525 0526 if (m_model->execute(m_proxy->mapToSource(ui.list->currentIndex()), filterText)) { 0527 if (!stillExists) { 0528 return true; // eat event 0529 } 0530 0531 if (!(keyEvent->modifiers() & Qt::ShiftModifier)) { 0532 emit ready(); 0533 } 0534 } else { 0535 //Maybe the filter-text was changed: 0536 if (filterText != ui.searchLine->text()) { 0537 ui.searchLine->setText(filterText); 0538 } 0539 } 0540 } 0541 return true; // eat event 0542 } 0543 } 0544 } 0545 0546 return QMenu::eventFilter(watched, event); 0547 } 0548 0549 #include "quickopenwidget.moc" 0550 #include "moc_quickopenwidget.cpp"