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 KToggleAction(i18n("Set &Bookmark"), this);
50 ac->addAction(QStringLiteral("bookmarks_toggle"), action: m_bookmarkToggle);
51 m_bookmarkToggle->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-new")));
52 ac->setDefaultShortcut(action: m_bookmarkToggle, shortcut: Qt::CTRL | Qt::Key_B);
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_bookmarkToggle->setChecked(m_view->doc()->mark(line: m_view->cursorPosition().line()) & KTextEditor::Document::markType01);
217 m_bookmarksMenu->addAction(action: m_bookmarkToggle);
218 m_bookmarksMenu->addAction(action: m_bookmarkClear);
219
220 m_goNext->setText(i18n("Next Bookmark"));
221 m_goPrevious->setText(i18n("Previous Bookmark"));
222
223 insertBookmarks(menu&: *m_bookmarksMenu);
224}
225
226void KateBookmarks::goNext()
227{
228 // reference ok, not modified
229 const auto &hash = m_view->doc()->marks();
230 if (hash.isEmpty()) {
231 return;
232 }
233
234 int line = m_view->cursorPosition().line();
235 int found = -1;
236 int firstBookmarkLine = -1;
237 for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
238 const int markLine = it.value()->line;
239 if (markLine > line && (found == -1 || found > markLine)) {
240 found = markLine;
241 }
242 if (markLine < firstBookmarkLine || firstBookmarkLine == -1) {
243 firstBookmarkLine = markLine;
244 }
245 }
246
247 // either go to next bookmark or the first in the document, bug 472354
248 if (found != -1) {
249 gotoLine(line: found);
250 } else {
251 gotoLine(line: firstBookmarkLine);
252 }
253}
254
255void KateBookmarks::goPrevious()
256{
257 // reference ok, not modified
258 const auto &hash = m_view->doc()->marks();
259 if (hash.isEmpty()) {
260 return;
261 }
262
263 int line = m_view->cursorPosition().line();
264 int found = -1;
265 int lastBookmarkLine = -1;
266 for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
267 const int markLine = it.value()->line;
268 if (markLine < line && (found == -1 || found < markLine)) {
269 found = markLine;
270 }
271 if (markLine > lastBookmarkLine) {
272 lastBookmarkLine = markLine;
273 }
274 }
275
276 // either go to previous bookmark or the last in the document, bug 472354
277 if (found != -1) {
278 gotoLine(line: found);
279 } else {
280 gotoLine(line: lastBookmarkLine);
281 }
282}
283
284void KateBookmarks::marksChanged()
285{
286 const bool bookmarks = !m_view->doc()->marks().isEmpty();
287 if (m_bookmarkClear) {
288 m_bookmarkClear->setEnabled(bookmarks);
289 }
290 if (m_goNext) {
291 m_goNext->setEnabled(bookmarks);
292 }
293 if (m_goPrevious) {
294 m_goPrevious->setEnabled(bookmarks);
295 }
296}
297

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