File indexing completed on 2024-04-14 03:55:28

0001 /*
0002     SPDX-FileCopyrightText: 2002, 2003, 2004 Anders Lund <anders.lund@lund.tdcadsl.dk>
0003     SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "katebookmarks.h"
0009 
0010 #include "kateabstractinputmode.h"
0011 #include "katedocument.h"
0012 #include "kateview.h"
0013 
0014 #include <KActionCollection>
0015 #include <KActionMenu>
0016 #include <KGuiItem>
0017 #include <KLocalizedString>
0018 #include <KStringHandler>
0019 #include <KToggleAction>
0020 #include <KXMLGUIClient>
0021 #include <KXMLGUIFactory>
0022 
0023 #include <QEvent>
0024 #include <QList>
0025 #include <QMenu>
0026 #include <QRegularExpression>
0027 
0028 namespace KTextEditor
0029 {
0030 class Document;
0031 }
0032 
0033 KateBookmarks::KateBookmarks(KTextEditor::ViewPrivate *view, Sorting sort)
0034     : QObject(view)
0035     , m_view(view)
0036     , m_bookmarkClear(nullptr)
0037     , m_sorting(sort)
0038 {
0039     setObjectName(QStringLiteral("kate bookmarks"));
0040     connect(view->doc(), &KTextEditor::DocumentPrivate::marksChanged, this, &KateBookmarks::marksChanged);
0041     _tries = 0;
0042     m_bookmarksMenu = nullptr;
0043 }
0044 
0045 KateBookmarks::~KateBookmarks() = default;
0046 
0047 void KateBookmarks::createActions(KActionCollection *ac)
0048 {
0049     m_bookmarkToggle = new KToggleAction(i18n("Set &Bookmark"), this);
0050     ac->addAction(QStringLiteral("bookmarks_toggle"), m_bookmarkToggle);
0051     m_bookmarkToggle->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-new")));
0052     ac->setDefaultShortcut(m_bookmarkToggle, Qt::CTRL | Qt::Key_B);
0053     m_bookmarkToggle->setWhatsThis(i18n("If a line has no bookmark then add one, otherwise remove it."));
0054     connect(m_bookmarkToggle, &QAction::triggered, this, &KateBookmarks::toggleBookmark);
0055 
0056     m_bookmarkClear = new QAction(i18n("Clear &All Bookmarks"), this);
0057     ac->addAction(QStringLiteral("bookmarks_clear"), m_bookmarkClear);
0058     m_bookmarkClear->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-remove")));
0059     m_bookmarkClear->setWhatsThis(i18n("Remove all bookmarks of the current document."));
0060     connect(m_bookmarkClear, &QAction::triggered, this, &KateBookmarks::clearBookmarks);
0061 
0062     m_goNext = new QAction(i18n("Next Bookmark"), this);
0063     ac->addAction(QStringLiteral("bookmarks_next"), m_goNext);
0064     m_goNext->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
0065     ac->setDefaultShortcut(m_goNext, Qt::ALT | Qt::Key_PageDown);
0066     m_goNext->setWhatsThis(i18n("Go to the next bookmark."));
0067     connect(m_goNext, &QAction::triggered, this, &KateBookmarks::goNext);
0068 
0069     m_goPrevious = new QAction(i18n("Previous Bookmark"), this);
0070     ac->addAction(QStringLiteral("bookmarks_previous"), m_goPrevious);
0071     m_goPrevious->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search")));
0072     ac->setDefaultShortcut(m_goPrevious, Qt::ALT | Qt::Key_PageUp);
0073     m_goPrevious->setWhatsThis(i18n("Go to the previous bookmark."));
0074     connect(m_goPrevious, &QAction::triggered, this, &KateBookmarks::goPrevious);
0075 
0076     KActionMenu *actionMenu = new KActionMenu(i18n("&Bookmarks"), this);
0077     actionMenu->setPopupMode(QToolButton::InstantPopup);
0078     actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("bookmarks")));
0079     ac->addAction(QStringLiteral("bookmarks"), actionMenu);
0080     m_bookmarksMenu = actionMenu->menu();
0081 
0082     connect(m_bookmarksMenu, &QMenu::aboutToShow, this, &KateBookmarks::bookmarkMenuAboutToShow);
0083     // Ensure the bookmarks menu is populated with at least the basic actions, otherwise macOS will not show it in the global menu bar.
0084     bookmarkMenuAboutToShow();
0085 
0086     marksChanged();
0087 
0088     // Always want the actions with shortcuts plugged into something so their shortcuts can work
0089     m_view->addAction(m_bookmarkToggle);
0090     m_view->addAction(m_bookmarkClear);
0091     m_view->addAction(m_goNext);
0092     m_view->addAction(m_goPrevious);
0093 }
0094 
0095 void KateBookmarks::toggleBookmark()
0096 {
0097     uint mark = m_view->doc()->mark(m_view->cursorPosition().line());
0098     if (mark & KTextEditor::Document::markType01) {
0099         m_view->doc()->removeMark(m_view->cursorPosition().line(), KTextEditor::Document::markType01);
0100     } else {
0101         m_view->doc()->addMark(m_view->cursorPosition().line(), KTextEditor::Document::markType01);
0102     }
0103 }
0104 
0105 void KateBookmarks::clearBookmarks()
0106 {
0107     // work on a COPY of the hash, the removing will modify it otherwise!
0108     const auto hash = m_view->doc()->marks();
0109     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
0110         m_view->doc()->removeMark(it.value()->line, KTextEditor::Document::markType01);
0111     }
0112 }
0113 
0114 void KateBookmarks::insertBookmarks(QMenu &menu)
0115 {
0116     const int line = m_view->cursorPosition().line();
0117     static const QRegularExpression re(QStringLiteral("&(?!&)"));
0118     int next = -1; // -1 means next bookmark doesn't exist
0119     int prev = -1; // -1 means previous bookmark doesn't exist
0120 
0121     // reference ok, not modified
0122     const auto &hash = m_view->doc()->marks();
0123     if (hash.isEmpty()) {
0124         return;
0125     }
0126 
0127     std::vector<int> bookmarkLineArray; // Array of line numbers which have bookmarks
0128 
0129     // Find line numbers where bookmarks are set & store those line numbers in bookmarkLineArray
0130     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
0131         if (it.value()->type & KTextEditor::Document::markType01) {
0132             bookmarkLineArray.push_back(it.value()->line);
0133         }
0134     }
0135 
0136     if (m_sorting == Position) {
0137         std::sort(bookmarkLineArray.begin(), bookmarkLineArray.end());
0138     }
0139 
0140     QAction *firstNewAction = menu.addSeparator();
0141     // Consider each line with a bookmark one at a time
0142     for (size_t i = 0; i < bookmarkLineArray.size(); ++i) {
0143         const int lineNo = bookmarkLineArray.at(i);
0144         // Get text in this particular line in a QString
0145         QFontMetrics fontMetrics(menu.fontMetrics());
0146         QString bText = fontMetrics.elidedText(m_view->doc()->line(lineNo), Qt::ElideRight, fontMetrics.maxWidth() * 32);
0147         bText.replace(re, QStringLiteral("&&")); // kill undesired accellerators!
0148         bText.replace(QLatin1Char('\t'), QLatin1Char(' ')); // kill tabs, as they are interpreted as shortcuts
0149 
0150         QAction *before = nullptr;
0151         if (m_sorting == Position) {
0152             // 3 actions already present
0153             if (size_t(menu.actions().size()) <= i + 3) {
0154                 before = nullptr;
0155             } else {
0156                 before = menu.actions().at(i + 3);
0157             }
0158         }
0159 
0160         const QString actionText(QStringLiteral("%1  %2  - \"%3\"").arg(QString::number(lineNo + 1), m_view->currentInputMode()->bookmarkLabel(lineNo), bText));
0161         // Adding action for this bookmark in menu
0162         if (before) {
0163             QAction *a = new QAction(actionText, &menu);
0164             menu.insertAction(before, a);
0165             connect(a, &QAction::triggered, this, [this, lineNo]() {
0166                 gotoLine(lineNo);
0167             });
0168 
0169             if (!firstNewAction) {
0170                 firstNewAction = a;
0171             }
0172         } else {
0173             menu.addAction(actionText, this, [this, lineNo]() {
0174                 gotoLine(lineNo);
0175             });
0176         }
0177 
0178         // Find the line number of previous & next bookmark (if present) in relation to the cursor
0179         if (lineNo < line) {
0180             if (prev == -1 || prev < lineNo) {
0181                 prev = lineNo;
0182             }
0183         } else if (lineNo > line) {
0184             if (next == -1 || next > lineNo) {
0185                 next = lineNo;
0186             }
0187         }
0188     }
0189 
0190     if (next != -1) {
0191         // Insert action for next bookmark
0192         m_goNext->setText(i18n("&Next: %1 - \"%2\"", next + 1, KStringHandler::rsqueeze(m_view->doc()->line(next), 24)));
0193         menu.insertAction(firstNewAction, m_goNext);
0194         firstNewAction = m_goNext;
0195     }
0196     if (prev != -1) {
0197         // Insert action for previous bookmark
0198         m_goPrevious->setText(i18n("&Previous: %1 - \"%2\"", prev + 1, KStringHandler::rsqueeze(m_view->doc()->line(prev), 24)));
0199         menu.insertAction(firstNewAction, m_goPrevious);
0200         firstNewAction = m_goPrevious;
0201     }
0202 
0203     if (next != -1 || prev != -1) {
0204         menu.insertSeparator(firstNewAction);
0205     }
0206 }
0207 
0208 void KateBookmarks::gotoLine(int line)
0209 {
0210     m_view->setCursorPosition(KTextEditor::Cursor(line, 0));
0211 }
0212 
0213 void KateBookmarks::bookmarkMenuAboutToShow()
0214 {
0215     m_bookmarksMenu->clear();
0216     m_bookmarkToggle->setChecked(m_view->doc()->mark(m_view->cursorPosition().line()) & KTextEditor::Document::markType01);
0217     m_bookmarksMenu->addAction(m_bookmarkToggle);
0218     m_bookmarksMenu->addAction(m_bookmarkClear);
0219 
0220     m_goNext->setText(i18n("Next Bookmark"));
0221     m_goPrevious->setText(i18n("Previous Bookmark"));
0222 
0223     insertBookmarks(*m_bookmarksMenu);
0224 }
0225 
0226 void KateBookmarks::goNext()
0227 {
0228     // reference ok, not modified
0229     const auto &hash = m_view->doc()->marks();
0230     if (hash.isEmpty()) {
0231         return;
0232     }
0233 
0234     int line = m_view->cursorPosition().line();
0235     int found = -1;
0236     int firstBookmarkLine = -1;
0237     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
0238         const int markLine = it.value()->line;
0239         if (markLine > line && (found == -1 || found > markLine)) {
0240             found = markLine;
0241         }
0242         if (markLine < firstBookmarkLine || firstBookmarkLine == -1) {
0243             firstBookmarkLine = markLine;
0244         }
0245     }
0246 
0247     // either go to next bookmark or the first in the document, bug 472354
0248     if (found != -1) {
0249         gotoLine(found);
0250     } else {
0251         gotoLine(firstBookmarkLine);
0252     }
0253 }
0254 
0255 void KateBookmarks::goPrevious()
0256 {
0257     // reference ok, not modified
0258     const auto &hash = m_view->doc()->marks();
0259     if (hash.isEmpty()) {
0260         return;
0261     }
0262 
0263     int line = m_view->cursorPosition().line();
0264     int found = -1;
0265     int lastBookmarkLine = -1;
0266     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
0267         const int markLine = it.value()->line;
0268         if (markLine < line && (found == -1 || found < markLine)) {
0269             found = markLine;
0270         }
0271         if (markLine > lastBookmarkLine) {
0272             lastBookmarkLine = markLine;
0273         }
0274     }
0275 
0276     // either go to previous bookmark or the last in the document, bug 472354
0277     if (found != -1) {
0278         gotoLine(found);
0279     } else {
0280         gotoLine(lastBookmarkLine);
0281     }
0282 }
0283 
0284 void KateBookmarks::marksChanged()
0285 {
0286     const bool bookmarks = !m_view->doc()->marks().isEmpty();
0287     if (m_bookmarkClear) {
0288         m_bookmarkClear->setEnabled(bookmarks);
0289     }
0290     if (m_goNext) {
0291         m_goNext->setEnabled(bookmarks);
0292     }
0293     if (m_goPrevious) {
0294         m_goPrevious->setEnabled(bookmarks);
0295     }
0296 }