1 /*
2     This file is part of the Okteta Kasten module, made within the KDE community.
3 
4     SPDX-FileCopyrightText: 2007-2009, 2012 Friedrich W. H. Kossebau <kossebau@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8 
9 #include "bookmarkscontroller.hpp"
10 
11 // controller
12 #include "bookmarkeditpopup.hpp"
13 // Okteta Kasten gui
14 #include <Kasten/Okteta/ByteArrayView>
15 // Okteta Kasten core
16 #include <Kasten/Okteta/ByteArrayDocument>
17 // Kasten core
18 #include <Kasten/AbstractModel>
19 // Okteta gui
20 #include <Okteta/OffsetFormat>
21 // Okteta core
22 #include <Okteta/TextByteArrayAnalyzer>
23 #include <Okteta/CharCodec>
24 #include <Okteta/Bookmarkable>
25 #include <Okteta/BookmarksConstIterator>
26 #include <Okteta/Bookmark>
27 #include <Okteta/AbstractByteArrayModel>
28 // KF
29 #include <KXMLGUIClient>
30 #include <KLocalizedString>
31 #include <KActionCollection>
32 #include <KStandardAction>
33 // Qt
34 #include <QAction>
35 
36 namespace Kasten {
37 
38 static constexpr char BookmarkListActionListId[] = "bookmark_list";
39 
40 // TODO: Sortieren nach Offset oder Zeit
41 
BookmarksController(KXMLGUIClient * guiClient)42 BookmarksController::BookmarksController(KXMLGUIClient* guiClient)
43     : mGuiClient(guiClient)
44 {
45     KActionCollection* actionCollection = mGuiClient->actionCollection();
46 
47     mCreateAction = KStandardAction::addBookmark(this, &BookmarksController::createBookmark, this);
48 
49     mDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-remove")),
50                                 i18nc("@action:inmenu", "Remove Bookmark"), this);
51     mDeleteAction->setObjectName(QStringLiteral("bookmark_remove"));
52     connect(mDeleteAction, &QAction::triggered, this, &BookmarksController::deleteBookmark);
53     actionCollection->setDefaultShortcut(mDeleteAction, Qt::CTRL | Qt::SHIFT | Qt::Key_B);
54 
55     mDeleteAllAction = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-remove")),
56                                    i18nc("@action:inmenu", "Remove All Bookmarks"), this);
57     mDeleteAllAction->setObjectName(QStringLiteral("bookmark_remove_all"));
58     connect(mDeleteAllAction, &QAction::triggered, this, &BookmarksController::deleteAllBookmarks);
59 //     actionCollection->setDefaultShortcut( mDeleteAllAction, Qt::CTRL | Qt::Key_G );
60 
61     mGotoNextBookmarkAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next")),
62                                           i18nc("@action:inmenu", "Go to Next Bookmark"), this);
63     mGotoNextBookmarkAction->setObjectName(QStringLiteral("bookmark_next"));
64     connect(mGotoNextBookmarkAction, &QAction::triggered, this, &BookmarksController::gotoNextBookmark);
65     actionCollection->setDefaultShortcut(mGotoNextBookmarkAction, Qt::ALT | Qt::Key_Down);
66 
67     mGotoPreviousBookmarkAction = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")),
68                                               i18nc("@action:inmenu", "Go to Previous Bookmark"), this);
69     mGotoPreviousBookmarkAction->setObjectName(QStringLiteral("bookmark_previous"));
70     connect(mGotoPreviousBookmarkAction, &QAction::triggered, this, &BookmarksController::gotoPreviousBookmark);
71     actionCollection->setDefaultShortcut(mGotoPreviousBookmarkAction, Qt::ALT | Qt::Key_Up);
72 
73     mBookmarksActionGroup = new QActionGroup(this);   // TODO: do we use this only for the signal mapping?
74 //     mBookmarksActionGroup->setExclusive( true );
75     connect(mBookmarksActionGroup, &QActionGroup::triggered,
76             this, &BookmarksController::onBookmarkTriggered);
77 
78     actionCollection->addActions({
79         mCreateAction,
80         mDeleteAction,
81         mDeleteAllAction,
82         mGotoNextBookmarkAction,
83         mGotoPreviousBookmarkAction
84     });
85 
86     setTargetModel(nullptr);
87 }
88 
89 BookmarksController::~BookmarksController() = default;
90 
setTargetModel(AbstractModel * model)91 void BookmarksController::setTargetModel(AbstractModel* model)
92 {
93     if (mByteArrayView) {
94         mByteArrayView->disconnect(this);
95     }
96     if (mByteArray) {
97         mByteArray->disconnect(this);
98     }
99 
100     mByteArrayView = model ? model->findBaseModel<ByteArrayView*>() : nullptr;
101 
102     ByteArrayDocument* document =
103         mByteArrayView ? qobject_cast<ByteArrayDocument*>(mByteArrayView->baseModel()) : nullptr;
104     mByteArray = document ? document->content() : nullptr;
105     mBookmarks = (mByteArray && mByteArrayView) ? qobject_cast<Okteta::Bookmarkable*>(mByteArray) : nullptr;
106 
107     const bool hasViewWithBookmarks = (mBookmarks != nullptr);
108     int bookmarksCount = 0;
109     if (hasViewWithBookmarks) {
110         bookmarksCount = mBookmarks->bookmarksCount();
111         connect(mByteArray, SIGNAL(bookmarksAdded(QVector<Okteta::Bookmark>)),
112                 SLOT(onBookmarksAdded(QVector<Okteta::Bookmark>)));
113         connect(mByteArray, SIGNAL(bookmarksRemoved(QVector<Okteta::Bookmark>)),
114                 SLOT(onBookmarksRemoved(QVector<Okteta::Bookmark>)));
115         connect(mByteArray, SIGNAL(bookmarksModified(QVector<int>)),
116                 SLOT(updateBookmarks()));
117         connect(mByteArrayView, &ByteArrayView::cursorPositionChanged,
118                 this, &BookmarksController::onCursorPositionChanged);
119         connect(mByteArrayView, &ByteArrayView::offsetCodingChanged,
120                 this, &BookmarksController::updateBookmarks);
121     }
122 
123     updateBookmarks();
124 
125     const bool hasBookmarks = hasViewWithBookmarks && (bookmarksCount != 0);
126     if (hasViewWithBookmarks) {
127         onCursorPositionChanged(mByteArrayView->cursorPosition());
128     } else {
129         mCreateAction->setEnabled(false);
130         mDeleteAction->setEnabled(false);
131     }
132     mDeleteAllAction->setEnabled(hasBookmarks);
133     mGotoNextBookmarkAction->setEnabled(hasBookmarks);
134     mGotoPreviousBookmarkAction->setEnabled(hasBookmarks);
135 }
136 
updateBookmarks()137 void BookmarksController::updateBookmarks()
138 {
139     mGuiClient->unplugActionList(QLatin1String(BookmarkListActionListId));
140 
141     qDeleteAll(mBookmarksActionGroup->actions());
142 
143     if (!mBookmarks) {
144         return;
145     }
146 
147     const int startOffset = mByteArrayView->startOffset();
148     Okteta::OffsetFormat::print printFunction =
149         Okteta::OffsetFormat::printFunction((Okteta::OffsetFormat::Format)mByteArrayView->offsetCoding());
150 
151     char codedOffset[Okteta::OffsetFormat::MaxFormatWidth + 1];
152 
153     constexpr int firstWithNumericShortCut = 1;
154     constexpr int lastWithNumericShortCut = 9;
155     int b = firstWithNumericShortCut;
156 
157     Okteta::BookmarksConstIterator bit = mBookmarks->createBookmarksConstIterator();
158     while (bit.hasNext()) {
159         const Okteta::Bookmark& bookmark = bit.next();
160         printFunction(codedOffset, startOffset + bookmark.offset());
161         QString title = i18nc("@item description of bookmark", "%1: %2", QString::fromUtf8(codedOffset), bookmark.name());
162         if (b <= lastWithNumericShortCut) {
163             title = QStringLiteral("&%1 %2").arg(b).arg(title);
164             // = KStringHandler::rsqueeze( view->title(), MaxEntryLength );
165             ++b;
166         }
167         auto* action = new QAction(title, mBookmarksActionGroup);
168 
169         action->setData(bookmark.offset());
170         mBookmarksActionGroup->addAction(action);
171     }
172     mGuiClient->plugActionList(QString::fromUtf8(BookmarkListActionListId),
173                                mBookmarksActionGroup->actions());
174 }
175 
onBookmarksAdded(const QVector<Okteta::Bookmark> & bookmarks)176 void BookmarksController::onBookmarksAdded(const QVector<Okteta::Bookmark>& bookmarks)
177 {
178     Q_UNUSED(bookmarks)
179     const int currentPosition = mByteArrayView->cursorPosition();
180     onCursorPositionChanged(currentPosition);
181 
182     const int bookmarksCount = mBookmarks->bookmarksCount();
183     const bool hasBookmarks = (bookmarksCount != 0);
184 
185     mDeleteAllAction->setEnabled(hasBookmarks);
186 
187     updateBookmarks();
188 }
189 
onBookmarksRemoved(const QVector<Okteta::Bookmark> & bookmarks)190 void BookmarksController::onBookmarksRemoved(const QVector<Okteta::Bookmark>& bookmarks)
191 {
192     Q_UNUSED(bookmarks)
193     const int currentPosition = mByteArrayView->cursorPosition();
194     onCursorPositionChanged(currentPosition);
195 
196     const int bookmarksCount = mBookmarks->bookmarksCount();
197     const bool hasBookmarks = (bookmarksCount != 0);
198 
199     mDeleteAllAction->setEnabled(hasBookmarks);
200 
201     updateBookmarks();
202 }
203 
onCursorPositionChanged(Okteta::Address newPosition)204 void BookmarksController::onCursorPositionChanged(Okteta::Address newPosition)
205 {
206     const int bookmarksCount = mBookmarks->bookmarksCount();
207     const bool hasBookmarks = (bookmarksCount != 0);
208     const bool isInsideByteArray = (newPosition < mByteArray->size());
209     bool isAtBookmark;
210     bool hasPrevious;
211     bool hasNext;
212     if (hasBookmarks) {
213         isAtBookmark = mBookmarks->containsBookmarkFor(newPosition);
214         Okteta::BookmarksConstIterator bookmarksIterator = mBookmarks->createBookmarksConstIterator();
215         hasPrevious = bookmarksIterator.findPreviousFrom(newPosition);
216         hasNext = bookmarksIterator.findNextFrom(newPosition);
217     } else {
218         isAtBookmark = false;
219         hasPrevious = false;
220         hasNext = false;
221     }
222 
223     mCreateAction->setEnabled(!isAtBookmark && isInsideByteArray);
224     mDeleteAction->setEnabled(isAtBookmark);
225     mGotoNextBookmarkAction->setEnabled(hasNext);
226     mGotoPreviousBookmarkAction->setEnabled(hasPrevious);
227 }
228 
createBookmark()229 void BookmarksController::createBookmark()
230 {
231     const int cursorPosition = mByteArrayView->cursorPosition();
232 
233     // search for text at cursor
234     const Okteta::CharCodec* charCodec = Okteta::CharCodec::createCodec(mByteArrayView->charCodingName());
235     const Okteta::TextByteArrayAnalyzer textAnalyzer(mByteArray, charCodec);
236     QString bookmarkName = textAnalyzer.text(cursorPosition, cursorPosition + MaxBookmarkNameSize - 1);
237     delete charCodec;
238 
239     if (bookmarkName.isEmpty()) {
240         bookmarkName = i18nc("default name of a bookmark", "Bookmark");  // %1").arg( 0 ) ); // TODO: use counter like with new file, globally
241 
242     }
243     auto* bookmarkEditPopup = new BookmarkEditPopup(mByteArrayView->widget());
244     QPoint popupPoint = mByteArrayView->cursorRect().topLeft();
245 //     popupPoint.ry() += mSlider->height() / 2;
246     popupPoint = mByteArrayView->widget()->mapToGlobal(popupPoint);
247 
248     bookmarkEditPopup->setPosition(popupPoint);
249     bookmarkEditPopup->setName(bookmarkName);
250     bookmarkEditPopup->setCursorPosition(cursorPosition);
251     connect(bookmarkEditPopup, &BookmarkEditPopup::bookmarkAccepted,
252             this, &BookmarksController::addBookmark);
253     bookmarkEditPopup->open();
254 }
255 
addBookmark(int cursorPosition,const QString & name)256 void BookmarksController::addBookmark(int cursorPosition, const QString& name)
257 {
258     Okteta::Bookmark bookmark(cursorPosition);
259     bookmark.setName(name);
260 
261     const QVector<Okteta::Bookmark> bookmarks { bookmark };
262     mBookmarks->addBookmarks(bookmarks);
263 }
264 
deleteBookmark()265 void BookmarksController::deleteBookmark()
266 {
267     const int cursorPosition = mByteArrayView->cursorPosition();
268     const QVector<Okteta::Bookmark> bookmarks { cursorPosition };
269     mBookmarks->removeBookmarks(bookmarks);
270 }
271 
deleteAllBookmarks()272 void BookmarksController::deleteAllBookmarks()
273 {
274     mBookmarks->removeAllBookmarks();
275 }
276 
gotoNextBookmark()277 void BookmarksController::gotoNextBookmark()
278 {
279     const int currentPosition = mByteArrayView->cursorPosition();
280 
281     Okteta::BookmarksConstIterator bookmarksIterator = mBookmarks->createBookmarksConstIterator();
282     const bool hasNext = bookmarksIterator.findNextFrom(currentPosition);
283     if (hasNext) {
284         const int newPosition = bookmarksIterator.next().offset();
285         mByteArrayView->setCursorPosition(newPosition);
286     }
287 }
288 
gotoPreviousBookmark()289 void BookmarksController::gotoPreviousBookmark()
290 {
291     const int currentPosition = mByteArrayView->cursorPosition();
292 
293     Okteta::BookmarksConstIterator bookmarksIterator = mBookmarks->createBookmarksConstIterator();
294     const bool hasPrevious = bookmarksIterator.findPreviousFrom(currentPosition);
295     if (hasPrevious) {
296         const int newPosition = bookmarksIterator.previous().offset();
297         mByteArrayView->setCursorPosition(newPosition);
298     }
299 }
300 
onBookmarkTriggered(QAction * action)301 void BookmarksController::onBookmarkTriggered(QAction* action)
302 {
303     const int newPosition = action->data().toInt();
304     mByteArrayView->setCursorPosition(newPosition);
305 }
306 
307 }
308