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 &currentItemName)
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"