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"