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
28namespace KTextEditor
29{
30class Document;
31}
32
33KateBookmarks::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
45KateBookmarks::~KateBookmarks() = default;
46
47void 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 *actionMenu = 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
95void 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
105void 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
114void KateBookmarks::insertBookmarks(QMenu &menu)
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
208void KateBookmarks::gotoLine(int line)
209{
210 m_view->setCursorPosition(KTextEditor::Cursor(line, 0));
211}
212
213void KateBookmarks::bookmarkMenuAboutToShow()
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
225void 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
254void 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
283void 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

source code of ktexteditor/src/utils/katebookmarks.cpp