File indexing completed on 2024-04-28 05:49:34
0001 /* 0002 SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com> 0003 SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 0006 #include "kateurlbar.h" 0007 #include "kateapp.h" 0008 #include "katemainwindow.h" 0009 #include "kateviewmanager.h" 0010 #include "ktexteditor_utils.h" 0011 0012 #include <KTextEditor/Document> 0013 #include <KTextEditor/Editor> 0014 #include <KTextEditor/View> 0015 0016 #include <KColorScheme> 0017 #include <KLocalizedString> 0018 0019 #include <QAbstractListModel> 0020 #include <QApplication> 0021 #include <QDir> 0022 #include <QHBoxLayout> 0023 #include <QIcon> 0024 #include <QKeyEvent> 0025 #include <QLabel> 0026 #include <QLineEdit> 0027 #include <QListView> 0028 #include <QMenu> 0029 #include <QMimeDatabase> 0030 #include <QPainter> 0031 #include <QScrollBar> 0032 #include <QSortFilterProxyModel> 0033 #include <QStackedWidget> 0034 #include <QStandardItemModel> 0035 #include <QStyledItemDelegate> 0036 #include <QTimer> 0037 #include <QToolButton> 0038 #include <QTreeView> 0039 #include <QUrl> 0040 0041 #include <KFuzzyMatcher> 0042 0043 using namespace std::chrono_literals; 0044 0045 class FuzzyFilterModel final : public QSortFilterProxyModel 0046 { 0047 Q_OBJECT 0048 public: 0049 explicit FuzzyFilterModel(QObject *parent = nullptr) 0050 : QSortFilterProxyModel(parent) 0051 { 0052 connect(this, &FuzzyFilterModel::modelAboutToBeReset, this, [this] { 0053 m_pattern.clear(); 0054 }); 0055 } 0056 0057 bool filterAcceptsRow(int row, const QModelIndex &parent) const override 0058 { 0059 if (m_pattern.isEmpty()) { 0060 return true; 0061 } 0062 0063 const auto index = sourceModel()->index(row, filterKeyColumn(), parent); 0064 const auto text = index.data(filterRole()).toString(); 0065 const auto res = KFuzzyMatcher::matchSimple(m_pattern, text); 0066 return res; 0067 } 0068 0069 void setFilterString(const QString &text) 0070 { 0071 beginResetModel(); 0072 m_pattern = text; 0073 endResetModel(); 0074 } 0075 0076 private: 0077 QString m_pattern; 0078 }; 0079 0080 class BaseFilterItemView : public QWidget 0081 { 0082 Q_OBJECT 0083 public: 0084 explicit BaseFilterItemView(QWidget *parent = nullptr) 0085 : QWidget(parent) 0086 { 0087 } 0088 0089 Q_SIGNALS: 0090 void returnPressed(const QModelIndex &, Qt::KeyboardModifiers); 0091 void clicked(const QModelIndex &, Qt::KeyboardModifiers); 0092 }; 0093 0094 template<class ItemView> 0095 class FilterableItemView : public BaseFilterItemView 0096 { 0097 public: 0098 explicit FilterableItemView(QWidget *parent = nullptr) 0099 : BaseFilterItemView(parent) 0100 { 0101 auto layout = new QVBoxLayout(this); 0102 layout->setContentsMargins({}); 0103 layout->setSpacing(0); 0104 layout->addWidget(&m_filterText); 0105 layout->addWidget(&m_itemView); 0106 0107 m_filterText.setReadOnly(true); 0108 m_filterText.hide(); 0109 0110 m_itemView.installEventFilter(this); 0111 m_itemView.viewport()->installEventFilter(this); 0112 setFocusProxy(&m_itemView); 0113 0114 m_proxyModel.setFilterKeyColumn(0); 0115 m_proxyModel.setFilterRole(Qt::DisplayRole); 0116 m_itemView.setModel(&m_proxyModel); 0117 connect(&m_filterText, &QLineEdit::textChanged, &m_proxyModel, &FuzzyFilterModel::setFilterString); 0118 connect(&m_filterText, &QLineEdit::textChanged, this, [this] { 0119 auto index = m_proxyModel.index(0, 0); 0120 if (index.isValid()) { 0121 m_itemView.setCurrentIndex(index); 0122 } 0123 }); 0124 } 0125 0126 void setModel(QAbstractItemModel *model) 0127 { 0128 m_proxyModel.setSourceModel(model); 0129 } 0130 0131 QAbstractItemModel *model() 0132 { 0133 return m_itemView.model(); 0134 } 0135 0136 ItemView *view() 0137 { 0138 return &m_itemView; 0139 } 0140 0141 void setCurrentIndex(const QModelIndex &index) 0142 { 0143 m_itemView.setCurrentIndex(index); 0144 } 0145 QModelIndex currentIndex() const 0146 { 0147 return m_itemView.currentIndex(); 0148 } 0149 0150 protected: 0151 bool eventFilter(QObject *o, QEvent *e) override 0152 { 0153 if (e->type() == QEvent::MouseButtonPress) { 0154 QMouseEvent *me = static_cast<QMouseEvent *>(e); 0155 if (me->button() == Qt::LeftButton) { 0156 const QModelIndex idx = m_itemView.indexAt(m_itemView.viewport()->mapFromGlobal(me->globalPosition().toPoint())); 0157 if (!idx.isValid()) { 0158 return QWidget::eventFilter(o, me); 0159 } 0160 m_filterText.hide(); 0161 Q_EMIT clicked(idx, me->modifiers()); 0162 } 0163 } 0164 0165 if (e->type() == QEvent::KeyPress) { 0166 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e); 0167 if ((keyEvent->modifiers() == Qt::NoModifier || keyEvent->modifiers() == Qt::SHIFT) && !keyEvent->text().isEmpty()) { 0168 QChar c = keyEvent->text().front(); 0169 if (c.isPrint()) { 0170 m_filterText.setText(m_filterText.text() + keyEvent->text()); 0171 if (!m_filterText.isVisible()) { 0172 m_filterText.show(); 0173 } 0174 return true; 0175 } else if (keyEvent->key() == Qt::Key_Backspace) { 0176 if (m_filterText.text().isEmpty()) { 0177 return QWidget::eventFilter(o, e); 0178 } 0179 m_filterText.setText(m_filterText.text().chopped(1)); 0180 if (m_filterText.text().isEmpty()) { 0181 m_filterText.hide(); 0182 } 0183 return true; 0184 } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { 0185 m_filterText.blockSignals(true); 0186 m_filterText.clear(); 0187 m_filterText.blockSignals(false); 0188 m_filterText.hide(); 0189 Q_EMIT returnPressed(m_itemView.currentIndex(), keyEvent->modifiers()); 0190 } 0191 } 0192 } 0193 0194 return QWidget::eventFilter(o, e); 0195 } 0196 0197 private: 0198 ItemView m_itemView; 0199 QLineEdit m_filterText; 0200 FuzzyFilterModel m_proxyModel; 0201 }; 0202 using FilterableListView = FilterableItemView<QListView>; 0203 using FilterableTreeView = FilterableItemView<QTreeView>; 0204 0205 class DirFilesModel : public QAbstractListModel 0206 { 0207 Q_OBJECT 0208 public: 0209 DirFilesModel(QObject *parent = nullptr) 0210 : QAbstractListModel(parent) 0211 { 0212 } 0213 0214 enum Role { FileInfo = Qt::UserRole + 1 }; 0215 0216 int rowCount(const QModelIndex & = {}) const override 0217 { 0218 return m_fileInfos.size(); 0219 } 0220 0221 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override 0222 { 0223 if (!index.isValid()) { 0224 return {}; 0225 } 0226 0227 const auto &fi = m_fileInfos.at(index.row()); 0228 if (role == Qt::DisplayRole) { 0229 return fi.fileName(); 0230 } else if (role == Qt::DecorationRole) { 0231 if (fi.isDir()) { 0232 return QIcon(QIcon::fromTheme(QStringLiteral("folder"))); 0233 } else if (fi.isFile()) { 0234 return QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(fi).iconName()); 0235 } 0236 } else if (role == FileInfo) { 0237 return QVariant::fromValue(fi); 0238 } else if (role == Qt::ForegroundRole) { 0239 // highlight already open documents 0240 if (KateApp::self()->documentManager()->findDocument(QUrl::fromLocalFile(fi.absoluteFilePath()))) { 0241 return KColorScheme().foreground(KColorScheme::PositiveText).color(); 0242 } 0243 } 0244 0245 return {}; 0246 } 0247 0248 void setDir(const QDir &dir) 0249 { 0250 m_fileInfos.clear(); 0251 m_currentDir = dir; 0252 0253 beginResetModel(); 0254 const auto fileInfos = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::Hidden); 0255 for (const auto &fi : fileInfos) { 0256 if (fi.isDir()) { 0257 m_fileInfos.push_back(fi); 0258 } else if (QMimeDatabase().mimeTypeForFile(fi).inherits(QStringLiteral("text/plain"))) { 0259 m_fileInfos.push_back(fi); 0260 } 0261 } 0262 endResetModel(); 0263 } 0264 0265 QDir dir() const 0266 { 0267 return m_currentDir; 0268 } 0269 0270 private: 0271 std::vector<QFileInfo> m_fileInfos; 0272 QDir m_currentDir; 0273 }; 0274 0275 class DirFilesList : public QMenu 0276 { 0277 Q_OBJECT 0278 public: 0279 DirFilesList(QWidget *parent) 0280 : QMenu(parent) 0281 { 0282 m_list.setModel(&m_model); 0283 m_list.view()->setResizeMode(QListView::Adjust); 0284 m_list.view()->setViewMode(QListView::ListMode); 0285 m_list.view()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0286 m_list.view()->setFrameStyle(QFrame::NoFrame); 0287 0288 auto *l = new QVBoxLayout(this); 0289 l->setContentsMargins({}); 0290 l->addWidget(&m_list); 0291 0292 m_list.view()->viewport()->installEventFilter(this); 0293 setFocusProxy(&m_list); 0294 0295 connect(&m_list, &FilterableListView::returnPressed, this, &DirFilesList::onClicked); 0296 connect(&m_list, &FilterableListView::clicked, this, &DirFilesList::onClicked); 0297 connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &DirFilesList::updatePalette, Qt::QueuedConnection); 0298 } 0299 0300 void updatePalette() 0301 { 0302 auto p = m_list.palette(); 0303 p.setBrush(QPalette::Base, palette().alternateBase()); 0304 m_list.setPalette(p); 0305 } 0306 0307 void setDir(const QDir &d, const QString ¤tItemName) 0308 { 0309 m_model.setDir(d); 0310 updateGeometry(); 0311 auto firstIndex = m_model.index(0, 0); 0312 if (!currentItemName.isEmpty() && firstIndex.isValid()) { 0313 const auto idxesToSelect = m_model.match(firstIndex, Qt::DisplayRole, currentItemName); 0314 if (!idxesToSelect.isEmpty() && idxesToSelect.constFirst().isValid()) { 0315 m_list.setCurrentIndex(idxesToSelect.constFirst()); 0316 } 0317 } else { 0318 m_list.setCurrentIndex(firstIndex); 0319 } 0320 } 0321 0322 void updateGeometry() 0323 { 0324 auto rowHeight = m_list.view()->sizeHintForRow(0); 0325 auto c = m_model.rowCount(); 0326 const auto h = rowHeight * c + 4; 0327 const auto vScroll = m_list.view()->verticalScrollBar(); 0328 int w = m_list.view()->sizeHintForColumn(0) + (vScroll ? vScroll->height() / 2 : 0); 0329 0330 setFixedSize(qMin(w, 500), qMin(h, 600)); 0331 } 0332 0333 void onClicked(const QModelIndex &idx, Qt::KeyboardModifiers mod) 0334 { 0335 if (!idx.isValid()) { 0336 return; 0337 } 0338 const auto fi = idx.data(DirFilesModel::FileInfo).value<QFileInfo>(); 0339 if (fi.isDir()) { 0340 setDir(QDir(fi.absoluteFilePath()), QString()); 0341 } else if (fi.isFile()) { 0342 const QUrl url = QUrl::fromLocalFile(fi.absoluteFilePath()); 0343 hide(); 0344 Q_EMIT openUrl(url, /*newtab=*/mod); 0345 } 0346 } 0347 0348 void keyPressEvent(QKeyEvent *ke) override 0349 { 0350 if (ke->key() == Qt::Key_Left || ke->key() == Qt::Key_Right) { 0351 hide(); 0352 Q_EMIT navigateLeftRight(ke->key()); 0353 } else if (ke->key() == Qt::Key_Escape) { 0354 hide(); 0355 ke->accept(); 0356 return; 0357 } else if (ke->key() == Qt::Key_Backspace) { 0358 auto dir = m_model.dir(); 0359 if (dir.cdUp()) { 0360 setDir(dir, QString()); 0361 } 0362 } 0363 QMenu::keyPressEvent(ke); 0364 } 0365 0366 Q_SIGNALS: 0367 void openUrl(const QUrl &url, Qt::KeyboardModifiers); 0368 void navigateLeftRight(int key); 0369 0370 private: 0371 FilterableListView m_list; 0372 DirFilesModel m_model; 0373 }; 0374 0375 class SymbolsTreeView : public QMenu 0376 { 0377 Q_OBJECT 0378 public: 0379 // Copied from LSPClientSymbolView 0380 enum Role { 0381 SymbolRange = Qt::UserRole, 0382 ScoreRole, //> Unused here 0383 IsPlaceholder 0384 }; 0385 SymbolsTreeView(QWidget *parent) 0386 : QMenu(parent) 0387 { 0388 m_tree.view()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0389 m_tree.view()->setFrameStyle(QFrame::NoFrame); 0390 m_tree.view()->setUniformRowHeights(true); 0391 m_tree.view()->setHeaderHidden(true); 0392 m_tree.view()->setTextElideMode(Qt::ElideRight); 0393 m_tree.view()->setRootIsDecorated(false); 0394 0395 auto *l = new QVBoxLayout(this); 0396 l->setContentsMargins({}); 0397 l->addWidget(&m_tree); 0398 setFocusProxy(&m_tree); 0399 m_tree.view()->installEventFilter(this); 0400 m_tree.view()->viewport()->installEventFilter(this); 0401 0402 connect(&m_tree, &FilterableTreeView::clicked, this, &SymbolsTreeView::onClicked); 0403 connect(&m_tree, &FilterableTreeView::returnPressed, this, &SymbolsTreeView::onClicked); 0404 connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &SymbolsTreeView::updatePalette, Qt::QueuedConnection); 0405 } 0406 0407 void updatePalette() 0408 { 0409 auto p = m_tree.palette(); 0410 p.setBrush(QPalette::Base, palette().alternateBase()); 0411 m_tree.setPalette(p); 0412 } 0413 0414 void setSymbolsModel(QAbstractItemModel *model, KTextEditor::View *v, const QString &text) 0415 { 0416 m_activeView = v; 0417 m_tree.setModel(model); 0418 m_tree.view()->expandAll(); 0419 const auto idxToSelect = model->match(model->index(0, 0), 0, text, 1, Qt::MatchExactly); 0420 if (!idxToSelect.isEmpty()) { 0421 m_tree.setCurrentIndex(idxToSelect.constFirst()); 0422 } 0423 updateGeometry(); 0424 } 0425 0426 bool eventFilter(QObject *o, QEvent *e) override 0427 { 0428 // Handling via event filter is necessary to bypass 0429 // QTreeView's own key event handling 0430 if (e->type() == QEvent::KeyPress) { 0431 if (handleKeyPressEvent(static_cast<QKeyEvent *>(e))) { 0432 return true; 0433 } 0434 } 0435 0436 return QMenu::eventFilter(o, e); 0437 } 0438 0439 bool handleKeyPressEvent(QKeyEvent *ke) 0440 { 0441 if (ke->key() == Qt::Key_Left) { 0442 hide(); 0443 Q_EMIT navigateLeftRight(ke->key()); 0444 return false; 0445 } else if (ke->key() == Qt::Key_Escape) { 0446 hide(); 0447 ke->accept(); 0448 return true; 0449 } 0450 return false; 0451 } 0452 0453 void updateGeometry() 0454 { 0455 const auto *model = m_tree.model(); 0456 const int rows = rowCount(model, {}); 0457 const int rowHeight = m_tree.view()->sizeHintForRow(0); 0458 const int maxHeight = rows * rowHeight; 0459 0460 setFixedSize(350, qMin(600, maxHeight)); 0461 } 0462 0463 void onClicked(const QModelIndex &idx) 0464 { 0465 const auto range = idx.data(SymbolRange).value<KTextEditor::Range>(); 0466 if (range.isValid()) { 0467 m_activeView->setCursorPosition(range.start()); 0468 } 0469 hide(); 0470 } 0471 0472 static QModelIndex symbolForCurrentLine(QAbstractItemModel *model, const QModelIndex &index, int line) 0473 { 0474 const int rowCount = model->rowCount(index); 0475 for (int i = 0; i < rowCount; ++i) { 0476 const auto idx = model->index(i, 0, index); 0477 if (idx.data(SymbolRange).value<KTextEditor::Range>().overlapsLine(line)) { 0478 return idx; 0479 } else if (model->hasChildren(idx)) { 0480 const auto childIdx = symbolForCurrentLine(model, idx, line); 0481 if (childIdx.isValid()) { 0482 return childIdx; 0483 } 0484 } 0485 } 0486 return {}; 0487 } 0488 0489 private: 0490 // row count that counts top level + 1 level down rows 0491 // needed to ensure we don't get strange heights for 0492 // cases where there are only a couple of top level symbols 0493 int rowCount(const QAbstractItemModel *model, const QModelIndex &index) 0494 { 0495 int rows = model->rowCount(index); 0496 int child_rows = 0; 0497 for (int i = 0; i < rows; ++i) { 0498 child_rows += rowCount(model, model->index(i, 0, index)); 0499 } 0500 return rows + child_rows; 0501 } 0502 0503 FilterableTreeView m_tree; 0504 QPointer<KTextEditor::View> m_activeView; 0505 0506 Q_SIGNALS: 0507 void navigateLeftRight(int key); 0508 }; 0509 0510 enum BreadCrumbRole { 0511 PathRole = Qt::UserRole + 1, 0512 IsSeparator, 0513 IsSymbolCrumb, 0514 }; 0515 0516 class BreadCrumbDelegate : public QStyledItemDelegate 0517 { 0518 Q_OBJECT 0519 public: 0520 using QStyledItemDelegate::QStyledItemDelegate; 0521 0522 void initStyleOption(QStyleOptionViewItem *o, const QModelIndex &idx) const override 0523 { 0524 QStyledItemDelegate::initStyleOption(o, idx); 0525 o->decorationAlignment = Qt::AlignCenter; 0526 // We always want this icon size and nothing bigger 0527 if (idx.data(BreadCrumbRole::IsSeparator).toBool()) { 0528 o->decorationSize = QSize(8, 8); 0529 } else { 0530 o->decorationSize = QSize(16, 16); 0531 } 0532 0533 if (o->state & QStyle::State_MouseOver) { 0534 // No hover feedback for separators 0535 if (idx.data(BreadCrumbRole::IsSeparator).toBool()) { 0536 o->state.setFlag(QStyle::State_MouseOver, false); 0537 o->state.setFlag(QStyle::State_Active, false); 0538 } else { 0539 o->palette.setBrush(QPalette::Text, o->palette.windowText()); 0540 } 0541 } 0542 } 0543 0544 QSize sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &idx) const override 0545 { 0546 const auto str = idx.data(Qt::DisplayRole).toString(); 0547 const int margin = opt.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin); 0548 if (!str.isEmpty()) { 0549 const int hMargin = margin + 1; 0550 auto size = QStyledItemDelegate::sizeHint(opt, idx); 0551 const int w = opt.fontMetrics.horizontalAdvance(str) + (2 * hMargin); 0552 size.rwidth() = w; 0553 0554 if (!idx.data(Qt::DecorationRole).isNull()) { 0555 size.rwidth() += 16 + (2 * margin); 0556 } 0557 0558 return size; 0559 } else if (!idx.data(Qt::DecorationRole).isNull()) { 0560 QSize s(8, 8); 0561 s = s.grownBy({margin, 0, margin, 0}); 0562 return s; 0563 } 0564 return QStyledItemDelegate::sizeHint(opt, idx); 0565 } 0566 }; 0567 0568 class BreadCrumbView : public QListView 0569 { 0570 Q_OBJECT 0571 public: 0572 BreadCrumbView(QWidget *parent, KateUrlBar *urlBar) 0573 : QListView(parent) 0574 , m_urlBar(urlBar) 0575 { 0576 setFlow(QListView::LeftToRight); 0577 setModel(&m_model); 0578 setFrameStyle(QFrame::NoFrame); 0579 setSelectionMode(QAbstractItemView::SingleSelection); 0580 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0581 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0582 setItemDelegate(new BreadCrumbDelegate(this)); 0583 setEditTriggers(QAbstractItemView::NoEditTriggers); 0584 setTextElideMode(Qt::ElideNone); 0585 setSpacing(0); 0586 0587 connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &BreadCrumbView::updatePalette, Qt::QueuedConnection); 0588 updatePalette(); 0589 0590 auto onConfigChanged = [this] { 0591 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General")); 0592 auto v = cg.readEntry("Show Symbol In Navigation Bar", true); 0593 if (v != m_showSymbolCrumb) { 0594 m_showSymbolCrumb = v; 0595 Q_EMIT requestRefresh(); 0596 } 0597 }; 0598 connect(KateApp::self(), &KateApp::configurationChanged, this, onConfigChanged); 0599 onConfigChanged(); 0600 0601 connect(this, &QListView::clicked, this, &BreadCrumbView::onClicked); 0602 } 0603 0604 void updatePalette() 0605 { 0606 auto pal = palette(); 0607 pal.setBrush(QPalette::Base, qobject_cast<QWidget *>(parent())->palette().window()); 0608 auto textColor = pal.windowText().color(); 0609 textColor = textColor.lightness() > 127 ? textColor.darker(150) : textColor.lighter(150); 0610 pal.setBrush(QPalette::Inactive, QPalette::Text, textColor); 0611 pal.setBrush(QPalette::Active, QPalette::Text, textColor); 0612 setPalette(pal); 0613 } 0614 0615 void setUrl(const QString &baseDir, const QUrl &url) 0616 { 0617 if (url.isEmpty()) { 0618 return; 0619 } 0620 0621 const QString s = url.toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile); 0622 const auto &dirs = splittedUrl(baseDir, s); 0623 0624 m_model.clear(); 0625 m_symbolsModel = nullptr; 0626 0627 size_t i = 0; 0628 QIcon seperator = m_urlBar->seperator(); 0629 for (const auto &dir : dirs) { 0630 auto item = new QStandardItem(dir.name); 0631 item->setData(dir.path, BreadCrumbRole::PathRole); 0632 m_model.appendRow(item); 0633 0634 if (i < dirs.size() - 1) { 0635 auto sep = new QStandardItem(seperator, {}); 0636 sep->setSelectable(false); 0637 sep->setData(true, BreadCrumbRole::IsSeparator); 0638 m_model.appendRow(sep); 0639 } else { 0640 // last item, which is the filename, show icon with it 0641 const auto icon = QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(s, QMimeDatabase::MatchExtension).iconName()); 0642 item->setIcon(icon); 0643 } 0644 i++; 0645 } 0646 0647 if (!m_showSymbolCrumb) { 0648 return; 0649 } 0650 0651 auto *mainWindow = m_urlBar->viewManager()->mainWindow(); 0652 QPointer<QObject> lsp = mainWindow->pluginView(QStringLiteral("lspclientplugin")); 0653 if (lsp) { 0654 addSymbolCrumb(lsp); 0655 } 0656 } 0657 0658 void setDir(const QString &base) 0659 { 0660 m_model.clear(); 0661 if (base.isEmpty()) { 0662 return; 0663 } 0664 0665 QDir d(base); 0666 0667 std::vector<DirNamePath> dirs; 0668 0669 QString dirName = d.dirName(); 0670 0671 while (d.cdUp()) { 0672 if (dirName.isEmpty()) { 0673 continue; 0674 } 0675 dirs.push_back({dirName, d.absolutePath()}); 0676 dirName = d.dirName(); 0677 } 0678 std::reverse(dirs.begin(), dirs.end()); 0679 0680 QIcon seperator = m_urlBar->seperator(); 0681 for (const auto &dir : dirs) { 0682 auto item = new QStandardItem(dir.name); 0683 item->setData(dir.path, BreadCrumbRole::PathRole); 0684 m_model.appendRow(item); 0685 0686 auto sep = new QStandardItem(seperator, {}); 0687 sep->setSelectable(false); 0688 sep->setData(true, BreadCrumbRole::IsSeparator); 0689 m_model.appendRow(sep); 0690 } 0691 } 0692 0693 void addSymbolCrumb(QObject *lsp) 0694 { 0695 QAbstractItemModel *model = nullptr; 0696 QMetaObject::invokeMethod(lsp, "documentSymbolsModel", Q_RETURN_ARG(QAbstractItemModel *, model)); 0697 m_symbolsModel = model; 0698 if (!model) { 0699 return; 0700 } 0701 0702 connect(m_symbolsModel, &QAbstractItemModel::modelReset, this, &BreadCrumbView::updateSymbolsCrumb); 0703 0704 if (model->rowCount({}) == 0) { 0705 return; 0706 } 0707 0708 const auto view = m_urlBar->viewManager()->activeView(); 0709 disconnect(m_connToView); 0710 if (view) { 0711 m_connToView = connect(view, &KTextEditor::View::cursorPositionChanged, this, &BreadCrumbView::updateSymbolsCrumb); 0712 } 0713 0714 const auto idx = getSymbolCrumbText(); 0715 if (!idx.isValid()) { 0716 return; 0717 } 0718 0719 // Add separator 0720 auto sep = new QStandardItem(QIcon(m_urlBar->seperator()), {}); 0721 sep->setSelectable(false); 0722 sep->setData(true, BreadCrumbRole::IsSeparator); 0723 m_model.appendRow(sep); 0724 0725 const auto icon = idx.data(Qt::DecorationRole).value<QIcon>(); 0726 const auto text = idx.data().toString(); 0727 auto *item = new QStandardItem(icon, text); 0728 item->setData(true, BreadCrumbRole::IsSymbolCrumb); 0729 m_model.appendRow(item); 0730 } 0731 0732 void updateSymbolsCrumb() 0733 { 0734 QStandardItem *item = m_model.item(m_model.rowCount() - 1, 0); 0735 0736 if (!m_urlBar->viewSpace()->isActiveSpace()) { 0737 if (item && item->data(BreadCrumbRole::IsSymbolCrumb).toBool()) { 0738 // we are not active viewspace, remove the symbol + separator from breadcrumb 0739 // This is important as LSP only gives us symbols for the current active view 0740 // which atm is in some other viewspace. 0741 // In future we might want to extend LSP to provide us models for documents 0742 // but for now this will do. 0743 qDeleteAll(m_model.takeRow(m_model.rowCount() - 1)); 0744 qDeleteAll(m_model.takeRow(m_model.rowCount() - 1)); 0745 } 0746 return; 0747 } 0748 0749 const auto idx = getSymbolCrumbText(); 0750 if (!idx.isValid()) { 0751 return; 0752 } 0753 0754 if (!item || !item->data(BreadCrumbRole::IsSymbolCrumb).toBool()) { 0755 // Add separator 0756 auto sep = new QStandardItem(QIcon(m_urlBar->seperator()), {}); 0757 sep->setSelectable(false); 0758 sep->setData(true, BreadCrumbRole::IsSeparator); 0759 m_model.appendRow(sep); 0760 0761 item = new QStandardItem; 0762 m_model.appendRow(item); 0763 item->setData(true, BreadCrumbRole::IsSymbolCrumb); 0764 } 0765 0766 const auto text = idx.data().toString(); 0767 const auto icon = idx.data(Qt::DecorationRole).value<QIcon>(); 0768 item->setText(text); 0769 item->setIcon(icon); 0770 } 0771 0772 QModelIndex getSymbolCrumbText() 0773 { 0774 if (!m_symbolsModel) { 0775 return {}; 0776 } 0777 0778 QModelIndex first = m_symbolsModel->index(0, 0); 0779 if (first.data(SymbolsTreeView::IsPlaceholder).toBool()) { 0780 return {}; 0781 } 0782 0783 const auto view = m_urlBar->viewSpace()->currentView(); 0784 int line = view ? view->cursorPosition().line() : 0; 0785 0786 QModelIndex idx; 0787 if (line > 0) { 0788 idx = SymbolsTreeView::symbolForCurrentLine(m_symbolsModel, idx, line); 0789 } else { 0790 idx = first; 0791 } 0792 0793 if (!idx.isValid()) { 0794 idx = first; 0795 } 0796 return idx; 0797 } 0798 0799 static bool IsSeparator(const QModelIndex &idx) 0800 { 0801 return idx.data(BreadCrumbRole::IsSeparator).toBool(); 0802 } 0803 0804 void keyPressEvent(QKeyEvent *ke) override 0805 { 0806 const auto key = ke->key(); 0807 auto current = currentIndex(); 0808 0809 if (key == Qt::Key_Left || key == Qt::Key_Right) { 0810 onNavigateLeftRight(key, false); 0811 } else if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Down) { 0812 Q_EMIT clicked(current); 0813 } else if (key == Qt::Key_Escape) { 0814 Q_EMIT unsetFocus(); 0815 } 0816 } 0817 0818 void onClicked(const QModelIndex &idx) 0819 { 0820 // Clicked on the symbol? 0821 if (m_symbolsModel && idx.data(BreadCrumbRole::IsSymbolCrumb).toBool()) { 0822 auto activeView = m_urlBar->viewSpace()->currentView(); 0823 if (!activeView) { 0824 // View must be there 0825 return; 0826 } 0827 SymbolsTreeView t(this); 0828 connect( 0829 &t, 0830 &SymbolsTreeView::navigateLeftRight, 0831 this, 0832 [this](int k) { 0833 onNavigateLeftRight(k, true); 0834 }, 0835 Qt::QueuedConnection); 0836 const QString symbolName = idx.data().toString(); 0837 t.setSymbolsModel(m_symbolsModel, activeView, symbolName); 0838 const auto pos = mapToGlobal(rectForIndex(idx).bottomLeft()); 0839 t.setFocus(); 0840 t.exec(pos); 0841 } 0842 0843 auto path = idx.data(BreadCrumbRole::PathRole).toString(); 0844 if (path.isEmpty()) { 0845 return; 0846 } 0847 0848 const auto pos = mapToGlobal(rectForIndex(idx).bottomLeft()); 0849 0850 m_isNavigating = true; 0851 0852 QDir d(path); 0853 DirFilesList m(this); 0854 connect(&m, &DirFilesList::openUrl, m_urlBar, &KateUrlBar::openUrlRequested); 0855 connect(&m, &DirFilesList::openUrl, this, &BreadCrumbView::unsetFocus); 0856 connect( 0857 &m, 0858 &DirFilesList::navigateLeftRight, 0859 this, 0860 [this](int k) { 0861 onNavigateLeftRight(k, true); 0862 }, 0863 Qt::QueuedConnection); 0864 m.setDir(d, idx.data().toString()); 0865 m.setFocus(); 0866 m.exec(pos); 0867 m_isNavigating = false; 0868 } 0869 0870 bool navigating() const 0871 { 0872 return m_isNavigating; 0873 } 0874 0875 void openLastIndex() 0876 { 0877 const auto last = m_model.index(m_model.rowCount() - 1, 0); 0878 if (last.isValid()) { 0879 setCurrentIndex(last); 0880 clicked(last); 0881 } 0882 } 0883 0884 void updateSeperatorIcon() 0885 { 0886 auto newSeperator = m_urlBar->seperator(); 0887 for (int i = 0; i < m_model.rowCount(); ++i) { 0888 auto item = m_model.item(i); 0889 if (item && item->data(BreadCrumbRole::IsSeparator).toBool()) { 0890 item->setIcon(newSeperator); 0891 } 0892 } 0893 } 0894 0895 int maxWidth() const 0896 { 0897 const auto rowCount = m_model.rowCount(); 0898 int w = 0; 0899 QStyleOptionViewItem opt; 0900 initViewItemOption(&opt); 0901 auto delegate = itemDelegate(); 0902 for (int i = 0; i < rowCount; ++i) { 0903 w += delegate->sizeHint(opt, m_model.index(i, 0)).width(); 0904 } 0905 return w; 0906 } 0907 0908 private: 0909 QModelIndex lastIndex() 0910 { 0911 return m_model.index(m_model.rowCount() - 1, 0); 0912 } 0913 0914 struct DirNamePath { 0915 QString name; 0916 QString path; 0917 }; 0918 0919 void onNavigateLeftRight(int key, bool open) 0920 { 0921 const auto current = currentIndex(); 0922 const int step = IsSeparator(current) ? 1 : 2; 0923 const int nextRow = key == Qt::Key_Left ? current.row() - step : current.row() + step; 0924 auto nextIndex = current.sibling(nextRow, 0); 0925 if (nextIndex.isValid()) { 0926 setCurrentIndex(nextIndex); 0927 if (open) { 0928 Q_EMIT clicked(currentIndex()); 0929 } 0930 } 0931 } 0932 0933 static std::vector<DirNamePath> splittedUrl(const QString &base, const QString &s) 0934 { 0935 QDir dir(s); 0936 const QString fileName = dir.dirName(); 0937 dir.cdUp(); 0938 const QString path = dir.absolutePath(); 0939 0940 std::vector<DirNamePath> dirsList; 0941 dirsList.push_back(DirNamePath{fileName, path}); 0942 0943 // arrived at base? 0944 if (dir.absolutePath() == base) { 0945 return dirsList; 0946 } 0947 0948 QString dirName = dir.dirName(); 0949 0950 while (dir.cdUp()) { 0951 if (dirName.isEmpty()) { 0952 continue; 0953 } 0954 0955 DirNamePath dnp{dirName, dir.absolutePath()}; 0956 dirsList.push_back(dnp); 0957 0958 dirName = dir.dirName(); 0959 0960 // arrived at base? 0961 if (dir.absolutePath() == base) { 0962 break; 0963 } 0964 } 0965 std::reverse(dirsList.begin(), dirsList.end()); 0966 return dirsList; 0967 } 0968 0969 KateUrlBar *const m_urlBar; 0970 QStandardItemModel m_model; 0971 QPointer<QAbstractItemModel> m_symbolsModel; 0972 QMetaObject::Connection m_connToView; // Only one conn at a time 0973 bool m_isNavigating = false; 0974 bool m_showSymbolCrumb = true; 0975 0976 Q_SIGNALS: 0977 void unsetFocus(); 0978 void requestRefresh(); 0979 }; 0980 0981 // TODO: Merge this class back into KateUrlBar 0982 class UrlbarContainer : public QWidget 0983 { 0984 Q_OBJECT 0985 public: 0986 UrlbarContainer(KateUrlBar *parent) 0987 : QWidget(parent) 0988 , m_urlBar(parent) 0989 , m_ellipses(new QToolButton(this)) 0990 , m_baseCrumbView(new BreadCrumbView(this, parent)) 0991 , m_mainCrumbView(new BreadCrumbView(this, parent)) 0992 { 0993 m_ellipses->setText(QStringLiteral("…")); 0994 m_ellipses->setAutoRaise(true); 0995 auto urlBarLayout = new QHBoxLayout(this); 0996 connect(m_ellipses, &QToolButton::clicked, this, [urlBarLayout, this] { 0997 if (m_currBaseDir.isEmpty()) { 0998 qWarning() << "Unexpected empty base dir"; 0999 return; 1000 } 1001 m_ellipses->hide(); 1002 urlBarLayout->removeWidget(m_ellipses); 1003 m_baseCrumbView->setDir(m_currBaseDir); 1004 auto s = m_baseCrumbView->sizeHint(); 1005 s.setHeight(height()); 1006 int w = m_baseCrumbView->maxWidth(); 1007 s.setWidth(w); 1008 m_baseCrumbView->setFixedSize(s); 1009 urlBarLayout->insertWidget(0, m_baseCrumbView); 1010 m_baseCrumbView->show(); 1011 }); 1012 m_baseCrumbView->hide(); 1013 m_baseCrumbView->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored); 1014 1015 // UrlBar 1016 urlBarLayout->setSpacing(0); 1017 urlBarLayout->setContentsMargins({}); 1018 urlBarLayout->addWidget(m_ellipses); 1019 urlBarLayout->addWidget(m_mainCrumbView); 1020 setFocusProxy(m_mainCrumbView); 1021 1022 connect(m_mainCrumbView, &BreadCrumbView::unsetFocus, this, [this] { 1023 m_urlBar->viewManager()->activeView()->setFocus(); 1024 }); 1025 connect(m_mainCrumbView, &BreadCrumbView::requestRefresh, this, [this] { 1026 auto vs = m_urlBar->viewSpace(); 1027 auto view = vs->currentView(); 1028 if (vs && view) { 1029 setUrl(view->document()); 1030 } 1031 }); 1032 1033 connect( 1034 KTextEditor::Editor::instance(), 1035 &KTextEditor::Editor::configChanged, 1036 this, 1037 [this] { 1038 m_sepPixmap = QPixmap(); 1039 initSeparatorIcon(); 1040 m_mainCrumbView->updateSeperatorIcon(); 1041 }, 1042 Qt::QueuedConnection); 1043 1044 connect(&m_fullPathHideTimer, &QTimer::timeout, this, &UrlbarContainer::hideFullPath); 1045 m_fullPathHideTimer.setInterval(1s); 1046 m_fullPathHideTimer.setSingleShot(true); 1047 } 1048 1049 void open() 1050 { 1051 if (m_mainCrumbView) { 1052 m_mainCrumbView->openLastIndex(); 1053 } 1054 } 1055 1056 void setUrl(KTextEditor::Document *doc) 1057 { 1058 const QString baseDir = Utils::projectBaseDirForDocument(doc); 1059 1060 m_currBaseDir = baseDir; 1061 if (m_currBaseDir.isEmpty()) { 1062 m_ellipses->hide(); 1063 m_baseCrumbView->hide(); 1064 } else { 1065 m_ellipses->show(); 1066 } 1067 m_mainCrumbView->setUrl(baseDir, doc->url()); 1068 } 1069 1070 QPixmap separatorPixmap() 1071 { 1072 if (m_sepPixmap.isNull()) { 1073 initSeparatorIcon(); 1074 } 1075 return m_sepPixmap; 1076 } 1077 1078 protected: 1079 void leaveEvent(QEvent *e) override 1080 { 1081 if (!m_baseCrumbView->navigating()) { 1082 m_fullPathHideTimer.start(); 1083 } 1084 QWidget::leaveEvent(e); 1085 } 1086 1087 void enterEvent(QEnterEvent *e) override 1088 { 1089 m_fullPathHideTimer.stop(); 1090 QWidget::leaveEvent(e); 1091 } 1092 1093 private: 1094 void hideFullPath() 1095 { 1096 if (m_currBaseDir.isEmpty()) { 1097 return; 1098 } 1099 m_baseCrumbView->hide(); 1100 layout()->removeWidget(m_baseCrumbView); 1101 static_cast<QHBoxLayout *>(layout())->insertWidget(0, m_ellipses); 1102 m_ellipses->show(); 1103 } 1104 1105 void initSeparatorIcon() 1106 { 1107 Q_ASSERT(m_sepPixmap.isNull()); 1108 const auto dpr = this->devicePixelRatioF(); 1109 m_sepPixmap = QPixmap(8 * dpr, 8 * dpr); 1110 m_sepPixmap.fill(Qt::transparent); 1111 1112 auto pal = palette(); 1113 auto textColor = pal.text().color(); 1114 textColor = textColor.lightness() > 127 ? textColor.darker(150) : textColor.lighter(150); 1115 pal.setColor(QPalette::ButtonText, textColor); 1116 pal.setColor(QPalette::WindowText, textColor); 1117 pal.setColor(QPalette::Text, textColor); 1118 1119 QPainter p(&m_sepPixmap); 1120 QStyleOption o; 1121 o.rect.setRect(0, 0, 8, 8); 1122 o.palette = pal; 1123 style()->drawPrimitive(QStyle::PE_IndicatorArrowRight, &o, &p, this); 1124 m_sepPixmap.setDevicePixelRatio(dpr); 1125 } 1126 1127 KateUrlBar *const m_urlBar; 1128 QToolButton *const m_ellipses; 1129 BreadCrumbView *const m_baseCrumbView; 1130 BreadCrumbView *const m_mainCrumbView; 1131 QTimer m_fullPathHideTimer; 1132 QPixmap m_sepPixmap; 1133 QString m_currBaseDir; 1134 }; 1135 1136 KateUrlBar::KateUrlBar(KateViewSpace *parent) 1137 : QWidget(parent) 1138 , m_stack(new QStackedWidget(this)) 1139 , m_urlBarView(new UrlbarContainer(this)) 1140 , m_untitledDocLabel(new QLabel(this)) 1141 , m_parentViewSpace(parent) 1142 { 1143 setContentsMargins({}); 1144 setFont(QApplication::font("QMenu")); // We wan't not the default system font 1145 setFixedHeight(fontMetrics().height()); 1146 1147 setupLayout(); 1148 1149 auto *vm = parent->viewManager(); 1150 connect(vm, &KateViewManager::viewChanged, this, &KateUrlBar::onViewChanged); 1151 1152 connect(vm, &KateViewManager::showUrlNavBarChanged, this, [this, vm](bool show) { 1153 setHidden(!show); 1154 if (show) { 1155 onViewChanged(vm->activeView()); 1156 } 1157 }); 1158 1159 setHidden(!vm->showUrlNavBar()); 1160 } 1161 1162 void KateUrlBar::open() 1163 { 1164 if (m_stack->currentWidget() == m_urlBarView) { 1165 m_urlBarView->open(); 1166 } 1167 } 1168 1169 KateViewManager *KateUrlBar::viewManager() 1170 { 1171 return m_parentViewSpace->viewManager(); 1172 } 1173 1174 KateViewSpace *KateUrlBar::viewSpace() 1175 { 1176 return m_parentViewSpace; 1177 } 1178 1179 QIcon KateUrlBar::seperator() 1180 { 1181 return m_urlBarView->separatorPixmap(); 1182 } 1183 1184 void KateUrlBar::setupLayout() 1185 { 1186 // Setup the stacked widget 1187 m_stack->addWidget(m_untitledDocLabel); 1188 m_stack->addWidget(m_urlBarView); 1189 1190 // MainLayout 1191 auto *layout = new QHBoxLayout(this); 1192 layout->setContentsMargins({}); 1193 layout->setSpacing(0); 1194 layout->addWidget(m_stack); 1195 1196 auto updatePalette = [this]() { 1197 auto pal = m_untitledDocLabel->palette(); 1198 auto textColor = pal.text().color(); 1199 textColor = textColor.lightness() > 127 ? textColor.darker(150) : textColor.lighter(150); 1200 pal.setBrush(QPalette::Active, QPalette::WindowText, textColor); 1201 m_untitledDocLabel->setPalette(pal); 1202 }; 1203 updatePalette(); 1204 connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updatePalette, Qt::QueuedConnection); 1205 } 1206 1207 void KateUrlBar::onViewChanged(KTextEditor::View *v) 1208 { 1209 // We are not active but we have a doc? => don't do anything 1210 // we check for a doc because we want to update the KateUrlBar 1211 // when kate starts 1212 if (!viewSpace()->isActiveSpace() && m_currentDoc) { 1213 return; 1214 } 1215 1216 if (!v) { 1217 updateForDocument(nullptr); 1218 // no view => show nothing 1219 m_untitledDocLabel->setText({}); 1220 m_stack->setCurrentWidget(m_untitledDocLabel); 1221 return; 1222 } 1223 1224 updateForDocument(v->document()); 1225 } 1226 1227 void KateUrlBar::updateForDocument(KTextEditor::Document *doc) 1228 { 1229 // always disconnect and perhaps set nullptr doc 1230 if (m_currentDoc) { 1231 disconnect(m_currentDoc, &KTextEditor::Document::documentUrlChanged, this, &KateUrlBar::updateForDocument); 1232 } 1233 m_currentDoc = doc; 1234 if (!doc) { 1235 return; 1236 } 1237 1238 // we want to watch for url changed 1239 connect(m_currentDoc, &KTextEditor::Document::documentUrlChanged, this, &KateUrlBar::updateForDocument); 1240 1241 if (m_currentDoc->url().isEmpty() || !m_currentDoc->url().isLocalFile()) { 1242 m_untitledDocLabel->setText(m_currentDoc->documentName()); 1243 m_stack->setCurrentWidget(m_untitledDocLabel); 1244 return; 1245 } 1246 1247 if (m_stack->currentWidget() != m_urlBarView) { 1248 m_stack->setCurrentWidget(m_urlBarView); 1249 } 1250 1251 auto *vm = viewManager(); 1252 if (vm && !vm->showUrlNavBar()) { 1253 return; 1254 } 1255 1256 m_urlBarView->setUrl(doc); 1257 } 1258 1259 #include "kateurlbar.moc" 1260 #include "moc_kateurlbar.cpp"