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 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 * = 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_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 | |
226 | void 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 | |
255 | void 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 | |
284 | void 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 | |