| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2002, 2003, 2004 Anders Lund <anders.lund@lund.tdcadsl.dk> |
| 3 | SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org> |
| 4 | |
| 5 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 6 | */ |
| 7 | |
| 8 | #include "katebookmarks.h" |
| 9 | |
| 10 | #include "kateabstractinputmode.h" |
| 11 | #include "katedocument.h" |
| 12 | #include "kateview.h" |
| 13 | |
| 14 | #include <KActionCollection> |
| 15 | #include <KActionMenu> |
| 16 | #include <KGuiItem> |
| 17 | #include <KLocalizedString> |
| 18 | #include <KStringHandler> |
| 19 | #include <KToggleAction> |
| 20 | #include <KXMLGUIClient> |
| 21 | #include <KXMLGUIFactory> |
| 22 | |
| 23 | #include <QEvent> |
| 24 | #include <QList> |
| 25 | #include <QMenu> |
| 26 | #include <QRegularExpression> |
| 27 | |
| 28 | namespace KTextEditor |
| 29 | { |
| 30 | class Document; |
| 31 | } |
| 32 | |
| 33 | KateBookmarks::KateBookmarks(KTextEditor::ViewPrivate *view, Sorting sort) |
| 34 | : QObject(view) |
| 35 | , m_view(view) |
| 36 | , m_bookmarkClear(nullptr) |
| 37 | , m_sorting(sort) |
| 38 | { |
| 39 | setObjectName(QStringLiteral("kate bookmarks" )); |
| 40 | connect(sender: view->doc(), signal: &KTextEditor::DocumentPrivate::marksChanged, context: this, slot: &KateBookmarks::marksChanged); |
| 41 | _tries = 0; |
| 42 | m_bookmarksMenu = nullptr; |
| 43 | } |
| 44 | |
| 45 | KateBookmarks::~KateBookmarks() = default; |
| 46 | |
| 47 | void KateBookmarks::createActions(KActionCollection *ac) |
| 48 | { |
| 49 | m_bookmarkToggle = new QAction(i18n("Toggle &Bookmark" ), this); |
| 50 | ac->addAction(QStringLiteral("bookmarks_toggle" ), action: m_bookmarkToggle); |
| 51 | ac->setDefaultShortcut(action: m_bookmarkToggle, shortcut: Qt::CTRL | Qt::Key_B); |
| 52 | m_bookmarkToggle->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-new" ))); |
| 53 | m_bookmarkToggle->setWhatsThis(i18n("If a line has no bookmark then add one, otherwise remove it." )); |
| 54 | connect(sender: m_bookmarkToggle, signal: &QAction::triggered, context: this, slot: &KateBookmarks::toggleBookmark); |
| 55 | |
| 56 | m_bookmarkClear = new QAction(i18n("Clear &All Bookmarks" ), this); |
| 57 | ac->addAction(QStringLiteral("bookmarks_clear" ), action: m_bookmarkClear); |
| 58 | m_bookmarkClear->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-remove" ))); |
| 59 | m_bookmarkClear->setWhatsThis(i18n("Remove all bookmarks of the current document." )); |
| 60 | connect(sender: m_bookmarkClear, signal: &QAction::triggered, context: this, slot: &KateBookmarks::clearBookmarks); |
| 61 | |
| 62 | m_goNext = new QAction(i18n("Next Bookmark" ), this); |
| 63 | ac->addAction(QStringLiteral("bookmarks_next" ), action: m_goNext); |
| 64 | m_goNext->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search" ))); |
| 65 | ac->setDefaultShortcut(action: m_goNext, shortcut: Qt::ALT | Qt::Key_PageDown); |
| 66 | m_goNext->setWhatsThis(i18n("Go to the next bookmark." )); |
| 67 | connect(sender: m_goNext, signal: &QAction::triggered, context: this, slot: &KateBookmarks::goNext); |
| 68 | |
| 69 | m_goPrevious = new QAction(i18n("Previous Bookmark" ), this); |
| 70 | ac->addAction(QStringLiteral("bookmarks_previous" ), action: m_goPrevious); |
| 71 | m_goPrevious->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search" ))); |
| 72 | ac->setDefaultShortcut(action: m_goPrevious, shortcut: Qt::ALT | Qt::Key_PageUp); |
| 73 | m_goPrevious->setWhatsThis(i18n("Go to the previous bookmark." )); |
| 74 | connect(sender: m_goPrevious, signal: &QAction::triggered, context: this, slot: &KateBookmarks::goPrevious); |
| 75 | |
| 76 | KActionMenu * = new KActionMenu(i18n("&Bookmarks" ), this); |
| 77 | actionMenu->setPopupMode(QToolButton::InstantPopup); |
| 78 | actionMenu->setIcon(QIcon::fromTheme(QStringLiteral("bookmarks" ))); |
| 79 | ac->addAction(QStringLiteral("bookmarks" ), action: actionMenu); |
| 80 | m_bookmarksMenu = actionMenu->menu(); |
| 81 | |
| 82 | connect(sender: m_bookmarksMenu, signal: &QMenu::aboutToShow, context: this, slot: &KateBookmarks::bookmarkMenuAboutToShow); |
| 83 | // Ensure the bookmarks menu is populated with at least the basic actions, otherwise macOS will not show it in the global menu bar. |
| 84 | bookmarkMenuAboutToShow(); |
| 85 | |
| 86 | marksChanged(); |
| 87 | |
| 88 | // Always want the actions with shortcuts plugged into something so their shortcuts can work |
| 89 | m_view->addAction(action: m_bookmarkToggle); |
| 90 | m_view->addAction(action: m_bookmarkClear); |
| 91 | m_view->addAction(action: m_goNext); |
| 92 | m_view->addAction(action: m_goPrevious); |
| 93 | } |
| 94 | |
| 95 | void KateBookmarks::toggleBookmark() |
| 96 | { |
| 97 | uint mark = m_view->doc()->mark(line: m_view->cursorPosition().line()); |
| 98 | if (mark & KTextEditor::Document::markType01) { |
| 99 | m_view->doc()->removeMark(line: m_view->cursorPosition().line(), markType: KTextEditor::Document::markType01); |
| 100 | } else { |
| 101 | m_view->doc()->addMark(line: m_view->cursorPosition().line(), markType: KTextEditor::Document::markType01); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | void KateBookmarks::clearBookmarks() |
| 106 | { |
| 107 | // work on a COPY of the hash, the removing will modify it otherwise! |
| 108 | const auto hash = m_view->doc()->marks(); |
| 109 | for (auto it = hash.cbegin(); it != hash.cend(); ++it) { |
| 110 | m_view->doc()->removeMark(line: it.value()->line, markType: KTextEditor::Document::markType01); |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | void KateBookmarks::(QMenu &) |
| 115 | { |
| 116 | const int line = m_view->cursorPosition().line(); |
| 117 | static const QRegularExpression re(QStringLiteral("&(?!&)" )); |
| 118 | int next = -1; // -1 means next bookmark doesn't exist |
| 119 | int prev = -1; // -1 means previous bookmark doesn't exist |
| 120 | |
| 121 | // reference ok, not modified |
| 122 | const auto &hash = m_view->doc()->marks(); |
| 123 | if (hash.isEmpty()) { |
| 124 | return; |
| 125 | } |
| 126 | |
| 127 | std::vector<int> bookmarkLineArray; // Array of line numbers which have bookmarks |
| 128 | |
| 129 | // Find line numbers where bookmarks are set & store those line numbers in bookmarkLineArray |
| 130 | for (auto it = hash.cbegin(); it != hash.cend(); ++it) { |
| 131 | if (it.value()->type & KTextEditor::Document::markType01) { |
| 132 | bookmarkLineArray.push_back(x: it.value()->line); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | if (m_sorting == Position) { |
| 137 | std::sort(first: bookmarkLineArray.begin(), last: bookmarkLineArray.end()); |
| 138 | } |
| 139 | |
| 140 | QAction *firstNewAction = menu.addSeparator(); |
| 141 | // Consider each line with a bookmark one at a time |
| 142 | for (size_t i = 0; i < bookmarkLineArray.size(); ++i) { |
| 143 | const int lineNo = bookmarkLineArray.at(n: i); |
| 144 | // Get text in this particular line in a QString |
| 145 | QFontMetrics fontMetrics(menu.fontMetrics()); |
| 146 | QString bText = fontMetrics.elidedText(text: m_view->doc()->line(line: lineNo), mode: Qt::ElideRight, width: fontMetrics.maxWidth() * 32); |
| 147 | bText.replace(re, QStringLiteral("&&" )); // kill undesired accellerators! |
| 148 | bText.replace(before: QLatin1Char('\t'), after: QLatin1Char(' ')); // kill tabs, as they are interpreted as shortcuts |
| 149 | |
| 150 | QAction *before = nullptr; |
| 151 | if (m_sorting == Position) { |
| 152 | // 3 actions already present |
| 153 | if (size_t(menu.actions().size()) <= i + 3) { |
| 154 | before = nullptr; |
| 155 | } else { |
| 156 | before = menu.actions().at(i: i + 3); |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | const QString actionText(QStringLiteral("%1 %2 - \"%3\"" ).arg(args: QString::number(lineNo + 1), args: m_view->currentInputMode()->bookmarkLabel(line: lineNo), args&: bText)); |
| 161 | // Adding action for this bookmark in menu |
| 162 | if (before) { |
| 163 | QAction *a = new QAction(actionText, &menu); |
| 164 | menu.insertAction(before, action: a); |
| 165 | connect(sender: a, signal: &QAction::triggered, context: this, slot: [this, lineNo]() { |
| 166 | gotoLine(line: lineNo); |
| 167 | }); |
| 168 | |
| 169 | if (!firstNewAction) { |
| 170 | firstNewAction = a; |
| 171 | } |
| 172 | } else { |
| 173 | menu.addAction(text: actionText, args: this, args: [this, lineNo]() { |
| 174 | gotoLine(line: lineNo); |
| 175 | }); |
| 176 | } |
| 177 | |
| 178 | // Find the line number of previous & next bookmark (if present) in relation to the cursor |
| 179 | if (lineNo < line) { |
| 180 | if (prev == -1 || prev < lineNo) { |
| 181 | prev = lineNo; |
| 182 | } |
| 183 | } else if (lineNo > line) { |
| 184 | if (next == -1 || next > lineNo) { |
| 185 | next = lineNo; |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | if (next != -1) { |
| 191 | // Insert action for next bookmark |
| 192 | m_goNext->setText(i18n("&Next: %1 - \"%2\"" , next + 1, KStringHandler::rsqueeze(m_view->doc()->line(next), 24))); |
| 193 | menu.insertAction(before: firstNewAction, action: m_goNext); |
| 194 | firstNewAction = m_goNext; |
| 195 | } |
| 196 | if (prev != -1) { |
| 197 | // Insert action for previous bookmark |
| 198 | m_goPrevious->setText(i18n("&Previous: %1 - \"%2\"" , prev + 1, KStringHandler::rsqueeze(m_view->doc()->line(prev), 24))); |
| 199 | menu.insertAction(before: firstNewAction, action: m_goPrevious); |
| 200 | firstNewAction = m_goPrevious; |
| 201 | } |
| 202 | |
| 203 | if (next != -1 || prev != -1) { |
| 204 | menu.insertSeparator(before: firstNewAction); |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | void KateBookmarks::gotoLine(int line) |
| 209 | { |
| 210 | m_view->setCursorPosition(KTextEditor::Cursor(line, 0)); |
| 211 | } |
| 212 | |
| 213 | void KateBookmarks::() |
| 214 | { |
| 215 | m_bookmarksMenu->clear(); |
| 216 | m_bookmarksMenu->addAction(action: m_bookmarkToggle); |
| 217 | m_bookmarksMenu->addAction(action: m_bookmarkClear); |
| 218 | |
| 219 | m_goNext->setText(i18n("Next Bookmark" )); |
| 220 | m_goPrevious->setText(i18n("Previous Bookmark" )); |
| 221 | |
| 222 | insertBookmarks(menu&: *m_bookmarksMenu); |
| 223 | } |
| 224 | |
| 225 | void KateBookmarks::goNext() |
| 226 | { |
| 227 | // reference ok, not modified |
| 228 | const auto &hash = m_view->doc()->marks(); |
| 229 | if (hash.isEmpty()) { |
| 230 | return; |
| 231 | } |
| 232 | |
| 233 | int line = m_view->cursorPosition().line(); |
| 234 | int found = -1; |
| 235 | int firstBookmarkLine = -1; |
| 236 | for (auto it = hash.cbegin(); it != hash.cend(); ++it) { |
| 237 | const int markLine = it.value()->line; |
| 238 | if (markLine > line && (found == -1 || found > markLine)) { |
| 239 | found = markLine; |
| 240 | } |
| 241 | if (markLine < firstBookmarkLine || firstBookmarkLine == -1) { |
| 242 | firstBookmarkLine = markLine; |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | // either go to next bookmark or the first in the document, bug 472354 |
| 247 | if (found != -1) { |
| 248 | gotoLine(line: found); |
| 249 | } else if (m_cycleThroughBookmarks) { |
| 250 | gotoLine(line: firstBookmarkLine); |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | void KateBookmarks::goPrevious() |
| 255 | { |
| 256 | // reference ok, not modified |
| 257 | const auto &hash = m_view->doc()->marks(); |
| 258 | if (hash.isEmpty()) { |
| 259 | return; |
| 260 | } |
| 261 | |
| 262 | int line = m_view->cursorPosition().line(); |
| 263 | int found = -1; |
| 264 | int lastBookmarkLine = -1; |
| 265 | for (auto it = hash.cbegin(); it != hash.cend(); ++it) { |
| 266 | const int markLine = it.value()->line; |
| 267 | if (markLine < line && (found == -1 || found < markLine)) { |
| 268 | found = markLine; |
| 269 | } |
| 270 | if (markLine > lastBookmarkLine) { |
| 271 | lastBookmarkLine = markLine; |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | // either go to previous bookmark or the last in the document, bug 472354 |
| 276 | if (found != -1) { |
| 277 | gotoLine(line: found); |
| 278 | } else if (m_cycleThroughBookmarks) { |
| 279 | gotoLine(line: lastBookmarkLine); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | void KateBookmarks::marksChanged() |
| 284 | { |
| 285 | const bool bookmarks = !m_view->doc()->marks().isEmpty(); |
| 286 | if (m_bookmarkClear) { |
| 287 | m_bookmarkClear->setEnabled(bookmarks); |
| 288 | } |
| 289 | if (m_goNext) { |
| 290 | m_goNext->setEnabled(bookmarks); |
| 291 | } |
| 292 | if (m_goPrevious) { |
| 293 | m_goPrevious->setEnabled(bookmarks); |
| 294 | } |
| 295 | } |
| 296 | |