File indexing completed on 2024-05-12 05:52:07

0001 /*
0002     SPDX-FileCopyrightText: 2007, 2009 Joseph Wenninger <jowenn@kde.org>
0003     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "katequickopen.h"
0009 #include "katequickopenmodel.h"
0010 
0011 #include "katemainwindow.h"
0012 #include "kateviewmanager.h"
0013 
0014 #include <ktexteditor/document.h>
0015 #include <ktexteditor/view.h>
0016 
0017 #include <KLocalizedString>
0018 #include <KPluginFactory>
0019 #include <KSharedConfig>
0020 
0021 #include <QCoreApplication>
0022 #include <QEvent>
0023 #include <QPainter>
0024 #include <QPointer>
0025 #include <QSortFilterProxyModel>
0026 #include <QStyledItemDelegate>
0027 #include <QToolBar>
0028 #include <QTreeView>
0029 
0030 #include <drawing_utils.h>
0031 #include <kfts_fuzzy_match.h>
0032 #include <qgraphicseffect.h>
0033 
0034 class QuickOpenFilterProxyModel final : public QSortFilterProxyModel
0035 {
0036 public:
0037     QuickOpenFilterProxyModel(QObject *parent = nullptr)
0038         : QSortFilterProxyModel(parent)
0039     {
0040     }
0041 
0042 protected:
0043     bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
0044     {
0045         auto sm = sourceModel();
0046         if (pattern.isEmpty()) {
0047             const bool l = static_cast<KateQuickOpenModel *>(sm)->isOpened(sourceLeft);
0048             const bool r = static_cast<KateQuickOpenModel *>(sm)->isOpened(sourceRight);
0049             return l < r;
0050         }
0051         const int l = static_cast<KateQuickOpenModel *>(sm)->idxScore(sourceLeft);
0052         const int r = static_cast<KateQuickOpenModel *>(sm)->idxScore(sourceRight);
0053         return l < r;
0054     }
0055 
0056     bool filterAcceptsRow(int sourceRow, const QModelIndex &parent) const override
0057     {
0058         if (pattern.isEmpty()) {
0059             return true;
0060         }
0061 
0062         if (filterMode == Wildcard) {
0063             return QSortFilterProxyModel::filterAcceptsRow(sourceRow, parent);
0064         }
0065 
0066         auto sm = static_cast<KateQuickOpenModel *>(sourceModel());
0067         if (!sm->isValid(sourceRow)) {
0068             return false;
0069         }
0070 
0071         QStringView fileNameMatchPattern = pattern;
0072         // When matching path, we want to match the last section of the pattern
0073         // with filenames. /path/to/file => pattern: file
0074         if (matchPath) {
0075             int lastSlash = pattern.lastIndexOf(QLatin1Char('/'));
0076             if (lastSlash != -1) {
0077                 fileNameMatchPattern = fileNameMatchPattern.mid(lastSlash + 1);
0078             }
0079         }
0080 
0081         const QString &name = sm->idxToFileName(sourceRow);
0082 
0083         int score = 0;
0084         bool res;
0085         // dont use the QStringView(QString) ctor
0086         if (fileNameMatchPattern.isEmpty()) {
0087             res = true;
0088         } else {
0089             res = filterByName(QStringView(name.data(), name.size()), fileNameMatchPattern, score);
0090         }
0091 
0092         // only match file path if needed
0093         if (matchPath && res) {
0094             int scorep = 0;
0095             QStringView path{sm->idxToFilePath(sourceRow)};
0096             bool resp = filterByPath(path, QStringView(pattern.data(), pattern.size()), scorep);
0097             score += scorep;
0098             res = resp;
0099             // zero out the score if didn't match
0100             score *= res;
0101         }
0102 
0103         if (res) {
0104             // extra points for opened files
0105             score += (sm->isOpened(sourceRow)) * (score / 6);
0106 
0107             // extra points if file exists in project root
0108             // This gives priority to the files at the root
0109             // of the project over others. This is important
0110             // because otherwise getting to root files may
0111             // not be that easy
0112             if (!matchPath) {
0113                 score += (sm->idxToFilePath(sourceRow) == name) * name.size();
0114             }
0115         }
0116         //         if (res && pattern == QStringLiteral(""))
0117         //             qDebug() << score << ", " << name << "==================== END\n";
0118 
0119         sm->setScoreForIndex(sourceRow, score);
0120 
0121         return res;
0122     }
0123 
0124 public Q_SLOTS:
0125     bool setFilterText(const QString &text)
0126     {
0127         // we don't want to trigger filtering if the user is just entering line:col
0128         const auto splitted = text.split(QLatin1Char(':')).at(0);
0129         if (splitted == pattern) {
0130             return false;
0131         }
0132 
0133         if (filterMode == Wildcard) {
0134             pattern = splitted;
0135             setFilterWildcard(pattern);
0136             return true;
0137         }
0138 
0139         beginResetModel();
0140         pattern = splitted;
0141         matchPath = pattern.contains(QLatin1Char('/'));
0142         endResetModel();
0143 
0144         return true;
0145     }
0146 
0147     void setFilterMode(FilterMode m)
0148     {
0149         beginResetModel();
0150         filterMode = m;
0151         if (!pattern.isEmpty() && m == Wildcard) {
0152             setFilterWildcard(pattern);
0153         }
0154         endResetModel();
0155     }
0156 
0157 private:
0158     static inline bool filterByPath(QStringView path, QStringView pattern, int &score)
0159     {
0160         return kfts::fuzzy_match(pattern, path, score);
0161     }
0162 
0163     static inline bool filterByName(QStringView name, QStringView pattern, int &score)
0164     {
0165         return kfts::fuzzy_match(pattern, name, score);
0166     }
0167 
0168 private:
0169     QString pattern;
0170     bool matchPath = false;
0171     FilterMode filterMode = Fuzzy;
0172 };
0173 
0174 class QuickOpenStyleDelegate : public QStyledItemDelegate
0175 {
0176 public:
0177     QuickOpenStyleDelegate(QObject *parent = nullptr)
0178         : QStyledItemDelegate(parent)
0179     {
0180     }
0181 
0182     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0183     {
0184         QStyleOptionViewItem options = option;
0185         initStyleOption(&options, index);
0186 
0187         QString name = index.data(KateQuickOpenModel::FileName).toString();
0188         QString path = index.data(KateQuickOpenModel::FilePath).toString();
0189 
0190         // only remove suffix, not where it might occur elsewhere
0191         const QString suffix = QStringLiteral("/") + name;
0192         if (path.endsWith(suffix)) {
0193             path.chop(suffix.size());
0194         }
0195 
0196         QTextCharFormat fmt;
0197         fmt.setForeground(options.palette.link().color());
0198         fmt.setFontWeight(QFont::Bold);
0199 
0200         const int nameLen = name.length();
0201         // space between name and path
0202         constexpr int space = 1;
0203         QList<QTextLayout::FormatRange> formats;
0204         QList<QTextLayout::FormatRange> pathFormats;
0205 
0206         if (m_filterMode == Fuzzy) {
0207             // collect formats
0208             int pos = m_filterString.lastIndexOf(QLatin1Char('/'));
0209             if (pos > -1) {
0210                 ++pos;
0211                 auto pattern = QStringView(m_filterString).mid(pos);
0212                 auto nameFormats = kfts::get_fuzzy_match_formats(pattern, name, 0, fmt);
0213                 formats.append(nameFormats);
0214             } else {
0215                 auto nameFormats = kfts::get_fuzzy_match_formats(m_filterString, name, 0, fmt);
0216                 formats.append(nameFormats);
0217             }
0218             QTextCharFormat boldFmt;
0219             boldFmt.setFontWeight(QFont::Bold);
0220             boldFmt.setFontPointSize(options.font.pointSize() - 1);
0221             pathFormats = kfts::get_fuzzy_match_formats(m_filterString, path, nameLen + space, boldFmt);
0222         }
0223 
0224         QTextCharFormat gray;
0225         gray.setForeground(Qt::gray);
0226         gray.setFontPointSize(options.font.pointSize() - 1);
0227         formats.append({nameLen + space, static_cast<int>(path.length()), gray});
0228         if (!pathFormats.isEmpty()) {
0229             formats.append(pathFormats);
0230         }
0231 
0232         painter->save();
0233 
0234         // paint background
0235         if (option.state & QStyle::State_Selected) {
0236             painter->fillRect(option.rect, option.palette.highlight());
0237         } else {
0238             painter->fillRect(option.rect, option.palette.base());
0239         }
0240 
0241         options.text = QString(); // clear old text
0242         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0243 
0244         // space for icon
0245         painter->translate(25, 0);
0246 
0247         // draw text
0248         Utils::paintItemViewText(painter, QString(name + QStringLiteral(" ") + path), options, formats);
0249 
0250         painter->restore();
0251     }
0252 
0253 public Q_SLOTS:
0254     void setFilterString(const QString &text)
0255     {
0256         m_filterString = text;
0257     }
0258 
0259     void setFilterMode(FilterMode m)
0260     {
0261         m_filterMode = m;
0262     }
0263 
0264 private:
0265     QString m_filterString;
0266     FilterMode m_filterMode;
0267 };
0268 
0269 Q_DECLARE_METATYPE(QPointer<KTextEditor::Document>)
0270 
0271 KateQuickOpen::KateQuickOpen(KateMainWindow *mainWindow)
0272     : QFrame(mainWindow)
0273     , m_mainWindow(mainWindow)
0274 {
0275     QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(this);
0276     e->setColor(palette().color(QPalette::Shadow));
0277     e->setOffset(2.);
0278     e->setBlurRadius(8.);
0279     setGraphicsEffect(e);
0280 
0281     setAutoFillBackground(true);
0282     setFrameShadow(QFrame::Raised);
0283     setFrameShape(QFrame::Box);
0284 
0285     // handle resizing
0286     mainWindow->installEventFilter(this);
0287 
0288     // ensure the components have some proper frame
0289     QVBoxLayout *layout = new QVBoxLayout();
0290     layout->setSpacing(0);
0291     layout->setContentsMargins(2, 2, 2, 2);
0292     setLayout(layout);
0293 
0294     m_inputLine = new QuickOpenLineEdit(this);
0295     setFocusProxy(m_inputLine);
0296 
0297     layout->addWidget(m_inputLine);
0298 
0299     m_listView = new QTreeView(this);
0300     layout->addWidget(m_listView, 1);
0301     m_listView->setTextElideMode(Qt::ElideLeft);
0302     m_listView->setUniformRowHeights(true);
0303 
0304     m_model = new KateQuickOpenModel(this);
0305     m_styleDelegate = new QuickOpenStyleDelegate(this);
0306     m_listView->setItemDelegate(m_styleDelegate);
0307 
0308     connect(m_inputLine, &QuickOpenLineEdit::textChanged, this, &KateQuickOpen::reselectFirst, Qt::QueuedConnection);
0309     connect(m_inputLine, &QuickOpenLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed);
0310     connect(m_inputLine, &QuickOpenLineEdit::listModeChanged, this, &KateQuickOpen::slotListModeChanged);
0311     connect(m_inputLine, &QuickOpenLineEdit::filterModeChanged, this, &KateQuickOpen::setFilterMode);
0312 
0313     connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed);
0314     connect(m_listView, &QTreeView::clicked, this, &KateQuickOpen::slotReturnPressed); // for single click
0315 
0316     m_inputLine->installEventFilter(this);
0317     m_listView->installEventFilter(this);
0318     m_listView->setHeaderHidden(true);
0319     m_listView->setRootIsDecorated(false);
0320     m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0321     m_listView->setModel(m_model);
0322 
0323     connect(m_inputLine, &QuickOpenLineEdit::textChanged, this, [this](const QString &text) {
0324         // We init the proxy model when there is something to filter
0325         if (!m_proxyModel) {
0326             m_proxyModel = new QuickOpenFilterProxyModel(this);
0327             m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0328             m_proxyModel->setFilterMode(m_inputLine->filterMode());
0329             bool filtered = m_proxyModel->setFilterText(text);
0330             m_proxyModel->setSourceModel(m_model);
0331             m_listView->setModel(m_proxyModel);
0332             if (filtered) {
0333                 m_styleDelegate->setFilterString(text);
0334                 m_listView->viewport()->update();
0335             }
0336         } else {
0337             if (m_proxyModel->setFilterText(text)) {
0338                 m_styleDelegate->setFilterString(text);
0339                 m_listView->viewport()->update();
0340             }
0341         }
0342     });
0343 
0344     setHidden(true);
0345 
0346     setFilterMode();
0347     m_model->setListMode(m_inputLine->listMode());
0348 
0349     // fill stuff
0350     updateState();
0351 }
0352 
0353 bool KateQuickOpen::eventFilter(QObject *obj, QEvent *event)
0354 {
0355     // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
0356     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
0357         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0358         if (obj == m_inputLine) {
0359             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
0360                 || (keyEvent->key() == Qt::Key_PageDown);
0361             if (forward2list) {
0362                 QCoreApplication::sendEvent(m_listView, event);
0363                 return true;
0364             }
0365 
0366         } else if (obj == m_listView) {
0367             const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
0368                 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
0369             if (forward2input) {
0370                 QCoreApplication::sendEvent(m_inputLine, event);
0371                 return true;
0372             }
0373         }
0374 
0375         if (keyEvent->key() == Qt::Key_Escape) {
0376             hide();
0377             deleteLater();
0378             return true;
0379         }
0380     }
0381 
0382     if (event->type() == QEvent::FocusOut && !(m_inputLine->hasFocus() || m_listView->hasFocus())) {
0383         hide();
0384         deleteLater();
0385         return true;
0386     }
0387 
0388     // handle resizing
0389     if (m_mainWindow == obj && event->type() == QEvent::Resize) {
0390         updateViewGeometry();
0391     }
0392 
0393     return QWidget::eventFilter(obj, event);
0394 }
0395 
0396 void KateQuickOpen::reselectFirst()
0397 {
0398     int first = 0;
0399     const auto *model = m_listView->model();
0400     if (m_mainWindow->viewManager()->views().size() > 1 && model->rowCount() > 1 && m_inputLine->text().isEmpty()) {
0401         first = 1;
0402     }
0403 
0404     QModelIndex index = model->index(first, 0);
0405     m_listView->setCurrentIndex(index);
0406 }
0407 
0408 void KateQuickOpen::updateState()
0409 {
0410     m_model->refresh(m_mainWindow);
0411     reselectFirst();
0412 
0413     updateViewGeometry();
0414     show();
0415     setFocus();
0416 }
0417 
0418 void KateQuickOpen::slotReturnPressed()
0419 {
0420     // save current position before opening new url for location history
0421     auto vm = m_mainWindow->viewManager();
0422     if (auto v = vm->activeView()) {
0423         vm->addPositionToHistory(v->document()->url(), v->cursorPosition());
0424     }
0425 
0426     // either get view via document pointer or url
0427     const QModelIndex index = m_listView->currentIndex();
0428     KTextEditor::View *view = nullptr;
0429     if (auto doc = index.data(KateQuickOpenModel::Document).value<KTextEditor::Document *>()) {
0430         view = m_mainWindow->activateView(doc);
0431     } else {
0432         view = m_mainWindow->wrapper()->openUrl(index.data(Qt::UserRole).toUrl());
0433     }
0434 
0435     const auto strs = m_inputLine->text().split(QLatin1Char(':'));
0436     if (view && strs.count() > 1) {
0437         // helper to convert String => Number
0438         auto stringToInt = [](const QString &s) {
0439             bool ok = false;
0440             const int num = s.toInt(&ok);
0441             return ok ? num : -1;
0442         };
0443         KTextEditor::Cursor cursor = KTextEditor::Cursor::invalid();
0444 
0445         // try to get line
0446         const int line = stringToInt(strs.at(1));
0447         cursor.setLine(line - 1);
0448 
0449         // if line is valid, try to see if we have column available as well
0450         if (line > -1 && strs.count() > 2) {
0451             const int col = stringToInt(strs.at(2));
0452             cursor.setColumn(col - 1);
0453         }
0454 
0455         // do we have valid line at least?
0456         if (line > -1) {
0457             view->setCursorPosition(cursor);
0458         }
0459     }
0460 
0461     hide();
0462     deleteLater();
0463 
0464     m_mainWindow->slotWindowActivated();
0465 
0466     // store the new position in location history
0467     if (view) {
0468         vm->addPositionToHistory(view->document()->url(), view->cursorPosition());
0469     }
0470 }
0471 
0472 void KateQuickOpen::slotListModeChanged(KateQuickOpenModel::List mode)
0473 {
0474     m_model->setListMode(mode);
0475     // this changes things again, needs refresh, let's go all the way
0476     updateState();
0477 }
0478 
0479 void KateQuickOpen::setFilterMode()
0480 {
0481     auto newMode = m_inputLine->filterMode();
0482     if (m_proxyModel) {
0483         m_proxyModel->setFilterMode(newMode);
0484     }
0485     m_listView->setSortingEnabled(newMode == Fuzzy);
0486     m_styleDelegate->setFilterMode(newMode);
0487     m_listView->viewport()->update();
0488 }
0489 
0490 static QRect getQuickOpenBoundingRect(QMainWindow *mainWindow)
0491 {
0492     QRect boundingRect = mainWindow->contentsRect();
0493 
0494     // exclude the menu bar from the bounding rect
0495     if (const QWidget *menuWidget = mainWindow->menuWidget()) {
0496         if (!menuWidget->isHidden()) {
0497             boundingRect.setTop(boundingRect.top() + menuWidget->height());
0498         }
0499     }
0500 
0501     // exclude any undocked toolbar from the bounding rect
0502     const QList<QToolBar *> toolBars = mainWindow->findChildren<QToolBar *>();
0503     for (QToolBar *toolBar : toolBars) {
0504         if (toolBar->isHidden() || toolBar->isFloating()) {
0505             continue;
0506         }
0507 
0508         if (mainWindow->toolBarArea(toolBar) == Qt::TopToolBarArea) {
0509             boundingRect.setTop(std::max(boundingRect.top(), toolBar->geometry().bottom()));
0510         }
0511     }
0512 
0513     return boundingRect;
0514 }
0515 
0516 void KateQuickOpen::updateViewGeometry()
0517 {
0518     const QRect boundingRect = getQuickOpenBoundingRect(m_mainWindow);
0519 
0520     static constexpr int minWidth = 500;
0521     const int maxWidth = boundingRect.width();
0522     const int preferredWidth = maxWidth / 2.4;
0523 
0524     static constexpr int minHeight = 250;
0525     const int maxHeight = boundingRect.height();
0526     const int preferredHeight = maxHeight / 2;
0527 
0528     const QSize size{std::min(maxWidth, std::max(preferredWidth, minWidth)), std::min(maxHeight, std::max(preferredHeight, minHeight))};
0529 
0530     // resize() doesn't work here, so use setFixedSize() instead
0531     setFixedSize(size);
0532 
0533     // set the position to the top-center of the parent
0534     // just below the menubar/toolbar (if any)
0535     const QPoint position{boundingRect.center().x() - size.width() / 2, boundingRect.y()};
0536 
0537     move(position);
0538 }
0539 
0540 #include "moc_katequickopen.cpp"