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"