1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "bookmarkmanager.h"
27 
28 #include "bookmark.h"
29 #include "bookmarksplugin.h"
30 #include "bookmarks_global.h"
31 
32 #include <coreplugin/editormanager/editormanager.h>
33 #include <coreplugin/icore.h>
34 #include <coreplugin/idocument.h>
35 #include <coreplugin/actionmanager/actionmanager.h>
36 #include <coreplugin/actionmanager/command.h>
37 #include <projectexplorer/projectexplorer.h>
38 #include <projectexplorer/session.h>
39 #include <texteditor/texteditor.h>
40 #include <utils/algorithm.h>
41 #include <utils/icon.h>
42 #include <utils/qtcassert.h>
43 #include <utils/checkablemessagebox.h>
44 #include <utils/theme/theme.h>
45 #include <utils/dropsupport.h>
46 #include <utils/utilsicons.h>
47 
48 #include <QAction>
49 #include <QContextMenuEvent>
50 #include <QDebug>
51 #include <QDialog>
52 #include <QDialogButtonBox>
53 #include <QDir>
54 #include <QFileInfo>
55 #include <QFormLayout>
56 #include <QLineEdit>
57 #include <QMenu>
58 #include <QPainter>
59 #include <QSpinBox>
60 #include <QToolButton>
61 
62 Q_DECLARE_METATYPE(Bookmarks::Internal::Bookmark*)
63 
64 using namespace ProjectExplorer;
65 using namespace Core;
66 using namespace Utils;
67 
68 namespace Bookmarks {
69 namespace Internal {
70 
BookmarkDelegate(QObject * parent)71 BookmarkDelegate::BookmarkDelegate(QObject *parent)
72     : QStyledItemDelegate(parent)
73 {
74 }
75 
sizeHint(const QStyleOptionViewItem & option,const QModelIndex & index) const76 QSize BookmarkDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
77 {
78     QStyleOptionViewItem opt = option;
79     initStyleOption(&opt, index);
80 
81     QFontMetrics fm(option.font);
82     QSize s;
83     s.setWidth(option.rect.width());
84     s.setHeight(fm.height() * 2 + 10);
85     return s;
86 }
87 
generateGradientPixmap(int width,int height,const QColor & color,bool selected) const88 void BookmarkDelegate::generateGradientPixmap(int width, int height, const QColor &color, bool selected) const
89 {
90     QColor c = color;
91     c.setAlpha(0);
92 
93     QPixmap pixmap(width+1, height);
94     pixmap.fill(c);
95 
96     QPainter painter(&pixmap);
97     painter.setPen(Qt::NoPen);
98 
99     QLinearGradient lg;
100     lg.setCoordinateMode(QGradient::ObjectBoundingMode);
101     lg.setFinalStop(1,0);
102 
103     lg.setColorAt(0, c);
104     lg.setColorAt(0.4, color);
105 
106     painter.setBrush(lg);
107     painter.drawRect(0, 0, width+1, height);
108 
109     if (selected)
110         m_selectedPixmap = pixmap;
111     else
112         m_normalPixmap = pixmap;
113 }
114 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const115 void BookmarkDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
116 {
117     QStyleOptionViewItem opt = option;
118     initStyleOption(&opt, index);
119     painter->save();
120 
121     QFontMetrics fm(opt.font);
122     static int lwidth = fm.horizontalAdvance(QLatin1String("8888")) + 18;
123 
124     QColor backgroundColor;
125     QColor textColor;
126 
127     bool selected = opt.state & QStyle::State_Selected;
128 
129     if (selected) {
130         painter->setBrush(opt.palette.highlight().color());
131         backgroundColor = opt.palette.highlight().color();
132         if (!m_selectedPixmap)
133             generateGradientPixmap(lwidth, fm.height()+1, backgroundColor, selected);
134     } else {
135         backgroundColor = opt.palette.window().color();
136         painter->setBrush(backgroundColor);
137         if (!m_normalPixmap)
138             generateGradientPixmap(lwidth, fm.height(), backgroundColor, selected);
139     }
140     painter->setPen(Qt::NoPen);
141     painter->drawRect(opt.rect);
142 
143     // Set Text Color
144     if (opt.state & QStyle::State_Selected)
145         textColor = opt.palette.highlightedText().color();
146     else
147         textColor = opt.palette.text().color();
148 
149     painter->setPen(textColor);
150 
151 
152     // TopLeft
153     QString topLeft = index.data(BookmarkManager::Filename).toString();
154     painter->drawText(6, 2 + opt.rect.top() + fm.ascent(), topLeft);
155 
156     QString topRight = index.data(BookmarkManager::LineNumber).toString();
157     // Check whether we need to be fancy and paint some background
158     int fwidth = fm.horizontalAdvance(topLeft);
159     if (fwidth + lwidth > opt.rect.width()) {
160         int left = opt.rect.right() - lwidth;
161         painter->drawPixmap(left, opt.rect.top(), selected ? m_selectedPixmap : m_normalPixmap);
162     }
163     // topRight
164     painter->drawText(opt.rect.right() - fm.horizontalAdvance(topRight) - 6,
165                       2 + opt.rect.top() + fm.ascent(), topRight);
166 
167     // Directory
168     QColor mix;
169     mix.setRgbF(0.7 * textColor.redF()   + 0.3 * backgroundColor.redF(),
170                 0.7 * textColor.greenF() + 0.3 * backgroundColor.greenF(),
171                 0.7 * textColor.blueF()  + 0.3 * backgroundColor.blueF());
172     painter->setPen(mix);
173 
174     QString lineText = index.data(BookmarkManager::Note).toString().trimmed();
175     if (lineText.isEmpty())
176         lineText = index.data(BookmarkManager::LineText).toString();
177 
178     painter->drawText(6, opt.rect.top() + fm.ascent() + fm.height() + 6, lineText);
179 
180     // Separator lines
181     const QRectF innerRect = QRectF(opt.rect).adjusted(0.5, 0.5, -0.5, -0.5);
182     painter->setPen(QColor::fromRgb(150,150,150));
183     painter->drawLine(innerRect.bottomLeft(), innerRect.bottomRight());
184     painter->restore();
185 }
186 
BookmarkView(BookmarkManager * manager)187 BookmarkView::BookmarkView(BookmarkManager *manager)  :
188     m_bookmarkContext(new IContext(this)),
189     m_manager(manager)
190 {
191     setWindowTitle(tr("Bookmarks"));
192 
193     m_bookmarkContext->setWidget(this);
194     m_bookmarkContext->setContext(Context(Constants::BOOKMARKS_CONTEXT));
195 
196     ICore::addContextObject(m_bookmarkContext);
197 
198     ListView::setModel(manager);
199 
200     setItemDelegate(new BookmarkDelegate(this));
201     setFrameStyle(QFrame::NoFrame);
202     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
203     setAttribute(Qt::WA_MacShowFocusRect, false);
204     setSelectionModel(manager->selectionModel());
205     setSelectionMode(QAbstractItemView::SingleSelection);
206     setSelectionBehavior(QAbstractItemView::SelectRows);
207     setDragEnabled(true);
208     setDragDropMode(QAbstractItemView::DragOnly);
209 
210     connect(this, &QAbstractItemView::clicked, this, &BookmarkView::gotoBookmark);
211     connect(this, &QAbstractItemView::activated, this, &BookmarkView::gotoBookmark);
212 }
213 
createToolBarWidgets()214 QList<QToolButton *> BookmarkView::createToolBarWidgets()
215 {
216     Command *prevCmd = ActionManager::command(Constants::BOOKMARKS_PREV_ACTION);
217     Command *nextCmd = ActionManager::command(Constants::BOOKMARKS_NEXT_ACTION);
218     QTC_ASSERT(prevCmd && nextCmd, return {});
219     auto prevButton = new QToolButton(this);
220     prevButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
221     prevButton->setDefaultAction(prevCmd->action());
222     auto nextButton = new QToolButton(this);
223     nextButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
224     nextButton->setDefaultAction(nextCmd->action());
225     return {prevButton, nextButton};
226 }
227 
contextMenuEvent(QContextMenuEvent * event)228 void BookmarkView::contextMenuEvent(QContextMenuEvent *event)
229 {
230     QMenu menu;
231     QAction *moveUp = menu.addAction(tr("Move Up"));
232     QAction *moveDown = menu.addAction(tr("Move Down"));
233     QAction *edit = menu.addAction(tr("&Edit"));
234     menu.addSeparator();
235     QAction *remove = menu.addAction(tr("&Remove"));
236     menu.addSeparator();
237     QAction *removeAll = menu.addAction(tr("Remove All"));
238 
239     m_contextMenuIndex = indexAt(event->pos());
240     if (!m_contextMenuIndex.isValid()) {
241         moveUp->setEnabled(false);
242         moveDown->setEnabled(false);
243         remove->setEnabled(false);
244         edit->setEnabled(false);
245     }
246 
247     if (model()->rowCount() == 0)
248         removeAll->setEnabled(false);
249 
250     connect(moveUp, &QAction::triggered, m_manager, &BookmarkManager::moveUp);
251     connect(moveDown, &QAction::triggered, m_manager, &BookmarkManager::moveDown);
252     connect(remove, &QAction::triggered, this, &BookmarkView::removeFromContextMenu);
253     connect(removeAll, &QAction::triggered, this, &BookmarkView::removeAll);
254     connect(edit, &QAction::triggered, m_manager, &BookmarkManager::edit);
255 
256     menu.exec(mapToGlobal(event->pos()));
257 }
258 
removeFromContextMenu()259 void BookmarkView::removeFromContextMenu()
260 {
261     removeBookmark(m_contextMenuIndex);
262 }
263 
removeBookmark(const QModelIndex & index)264 void BookmarkView::removeBookmark(const QModelIndex& index)
265 {
266     Bookmark *bm = m_manager->bookmarkForIndex(index);
267     m_manager->deleteBookmark(bm);
268 }
269 
keyPressEvent(QKeyEvent * event)270 void BookmarkView::keyPressEvent(QKeyEvent *event)
271 {
272     if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
273         removeBookmark(currentIndex());
274         event->accept();
275         return;
276     }
277     ListView::keyPressEvent(event);
278 }
279 
removeAll()280 void BookmarkView::removeAll()
281 {
282     if (CheckableMessageBox::doNotAskAgainQuestion(this,
283             tr("Remove All Bookmarks"),
284             tr("Are you sure you want to remove all bookmarks from all files in the current session?"),
285             ICore::settings(),
286             QLatin1String("RemoveAllBookmarks")) != QDialogButtonBox::Yes)
287         return;
288 
289     // The performance of this function could be greatly improved.
290     while (m_manager->rowCount()) {
291         QModelIndex index = m_manager->index(0, 0);
292         removeBookmark(index);
293     }
294 }
295 
gotoBookmark(const QModelIndex & index)296 void BookmarkView::gotoBookmark(const QModelIndex &index)
297 {
298     Bookmark *bk = m_manager->bookmarkForIndex(index);
299     if (!m_manager->gotoBookmark(bk))
300         m_manager->deleteBookmark(bk);
301 }
302 
303 ////
304 // BookmarkManager
305 ////
306 
BookmarkManager()307 BookmarkManager::BookmarkManager() :
308     m_selectionModel(new QItemSelectionModel(this, this))
309 {
310     connect(ICore::instance(), &ICore::contextChanged,
311             this, &BookmarkManager::updateActionStatus);
312 
313     connect(SessionManager::instance(), &SessionManager::sessionLoaded,
314             this, &BookmarkManager::loadBookmarks);
315 
316     updateActionStatus();
317 }
318 
~BookmarkManager()319 BookmarkManager::~BookmarkManager()
320 {
321     qDeleteAll(m_bookmarksList);
322 }
323 
selectionModel() const324 QItemSelectionModel *BookmarkManager::selectionModel() const
325 {
326     return m_selectionModel;
327 }
328 
hasBookmarkInPosition(const Utils::FilePath & fileName,int lineNumber)329 bool BookmarkManager::hasBookmarkInPosition(const Utils::FilePath &fileName, int lineNumber)
330 {
331     return findBookmark(fileName, lineNumber);
332 }
333 
index(int row,int column,const QModelIndex & parent) const334 QModelIndex BookmarkManager::index(int row, int column, const QModelIndex &parent) const
335 {
336     if (parent.isValid())
337         return QModelIndex();
338     else
339         return createIndex(row, column);
340 }
341 
parent(const QModelIndex &) const342 QModelIndex BookmarkManager::parent(const QModelIndex &) const
343 {
344     return QModelIndex();
345 }
346 
rowCount(const QModelIndex & parent) const347 int BookmarkManager::rowCount(const QModelIndex &parent) const
348 {
349     if (parent.isValid())
350         return 0;
351     else
352         return m_bookmarksList.count();
353 }
354 
columnCount(const QModelIndex & parent) const355 int BookmarkManager::columnCount(const QModelIndex &parent) const
356 {
357     if (parent.isValid())
358         return 0;
359     return 3;
360 }
361 
data(const QModelIndex & index,int role) const362 QVariant BookmarkManager::data(const QModelIndex &index, int role) const
363 {
364     if (!index.isValid() || index.column() !=0 || index.row() < 0 || index.row() >= m_bookmarksList.count())
365         return QVariant();
366 
367     Bookmark *bookMark = m_bookmarksList.at(index.row());
368     if (role == BookmarkManager::Filename)
369         return bookMark->fileName().fileName();
370     if (role == BookmarkManager::LineNumber)
371         return bookMark->lineNumber();
372     if (role == BookmarkManager::Directory)
373         return bookMark->fileName().toFileInfo().path();
374     if (role == BookmarkManager::LineText)
375         return bookMark->lineText();
376     if (role == BookmarkManager::Note)
377         return bookMark->note();
378     if (role == Qt::ToolTipRole)
379         return bookMark->fileName().toUserOutput();
380     return QVariant();
381 }
382 
flags(const QModelIndex & index) const383 Qt::ItemFlags BookmarkManager::flags(const QModelIndex &index) const
384 {
385     if (!index.isValid() || index.column() !=0 || index.row() < 0 || index.row() >= m_bookmarksList.count())
386         return Qt::NoItemFlags;
387     return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled;
388 }
389 
supportedDragActions() const390 Qt::DropActions BookmarkManager::supportedDragActions() const
391 {
392     return Qt::MoveAction;
393 }
394 
mimeTypes() const395 QStringList BookmarkManager::mimeTypes() const
396 {
397     return DropSupport::mimeTypesForFilePaths();
398 }
399 
mimeData(const QModelIndexList & indexes) const400 QMimeData *BookmarkManager::mimeData(const QModelIndexList &indexes) const
401 {
402     auto data = new DropMimeData;
403     foreach (const QModelIndex &index, indexes) {
404         if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= m_bookmarksList.count())
405             continue;
406         Bookmark *bookMark = m_bookmarksList.at(index.row());
407         data->addFile(bookMark->fileName().toString(), bookMark->lineNumber());
408     }
409     return data;
410 }
411 
toggleBookmark(const FilePath & fileName,int lineNumber)412 void BookmarkManager::toggleBookmark(const FilePath &fileName, int lineNumber)
413 {
414     if (lineNumber <= 0 || fileName.isEmpty())
415         return;
416 
417     // Remove any existing bookmark on this line
418     if (Bookmark *mark = findBookmark(fileName, lineNumber)) {
419         // TODO check if the bookmark is really on the same markable Interface
420         deleteBookmark(mark);
421         return;
422     }
423 
424     // Add a new bookmark if no bookmark existed on this line
425     auto mark = new Bookmark(lineNumber, this);
426     mark->updateFileName(fileName);
427     const QModelIndex currentIndex = selectionModel()->currentIndex();
428     const int insertionIndex = currentIndex.isValid() ? currentIndex.row() + 1
429                                                       : m_bookmarksList.size();
430     insertBookmark(insertionIndex, mark);
431 }
432 
updateBookmark(Bookmark * bookmark)433 void BookmarkManager::updateBookmark(Bookmark *bookmark)
434 {
435     const int idx = m_bookmarksList.indexOf(bookmark);
436     if (idx == -1)
437         return;
438 
439     emit dataChanged(index(idx, 0, QModelIndex()), index(idx, 2, QModelIndex()));
440     saveBookmarks();
441 }
442 
updateBookmarkFileName(Bookmark * bookmark,const QString & oldFileName)443 void BookmarkManager::updateBookmarkFileName(Bookmark *bookmark, const QString &oldFileName)
444 {
445     if (oldFileName == bookmark->fileName().toString())
446         return;
447 
448     m_bookmarksMap[Utils::FilePath::fromString(oldFileName)].removeAll(bookmark);
449     m_bookmarksMap[bookmark->fileName()].append(bookmark);
450     updateBookmark(bookmark);
451 }
452 
removeAllBookmarks()453 void BookmarkManager::removeAllBookmarks()
454 {
455     if (m_bookmarksList.isEmpty())
456         return;
457     beginRemoveRows(QModelIndex(), 0, m_bookmarksList.size() - 1);
458 
459     qDeleteAll(m_bookmarksList);
460     m_bookmarksMap.clear();
461     m_bookmarksList.clear();
462     endRemoveRows();
463 }
464 
deleteBookmark(Bookmark * bookmark)465 void BookmarkManager::deleteBookmark(Bookmark *bookmark)
466 {
467     int idx = m_bookmarksList.indexOf(bookmark);
468     beginRemoveRows(QModelIndex(), idx, idx);
469 
470     m_bookmarksMap[bookmark->fileName()].removeAll(bookmark);
471     delete bookmark;
472 
473     m_bookmarksList.removeAt(idx);
474     endRemoveRows();
475 
476     if (selectionModel()->currentIndex().isValid())
477         selectionModel()->setCurrentIndex(selectionModel()->currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Clear);
478 
479     updateActionStatus();
480     saveBookmarks();
481 }
482 
bookmarkForIndex(const QModelIndex & index) const483 Bookmark *BookmarkManager::bookmarkForIndex(const QModelIndex &index) const
484 {
485     if (!index.isValid() || index.row() >= m_bookmarksList.size())
486         return nullptr;
487     return m_bookmarksList.at(index.row());
488 }
489 
gotoBookmark(const Bookmark * bookmark) const490 bool BookmarkManager::gotoBookmark(const Bookmark *bookmark) const
491 {
492     if (IEditor *editor = EditorManager::openEditorAt(
493             Utils::Link(bookmark->fileName(), bookmark->lineNumber()))) {
494         return editor->currentLine() == bookmark->lineNumber();
495     }
496     return false;
497 }
498 
nextInDocument()499 void BookmarkManager::nextInDocument()
500 {
501     documentPrevNext(true);
502 }
503 
prevInDocument()504 void BookmarkManager::prevInDocument()
505 {
506     documentPrevNext(false);
507 }
508 
documentPrevNext(bool next)509 void BookmarkManager::documentPrevNext(bool next)
510 {
511     IEditor *editor = EditorManager::currentEditor();
512     const int editorLine = editor->currentLine();
513     if (editorLine <= 0)
514         return;
515 
516     const FilePath filePath = editor->document()->filePath();
517     if (!m_bookmarksMap.contains(filePath))
518         return;
519 
520     int firstLine = -1;
521     int lastLine = -1;
522     int prevLine = -1;
523     int nextLine = -1;
524     const QVector<Bookmark *> marks = m_bookmarksMap[filePath];
525     for (int i = 0; i < marks.count(); ++i) {
526         int markLine = marks.at(i)->lineNumber();
527         if (firstLine == -1 || firstLine > markLine)
528             firstLine = markLine;
529         if (lastLine < markLine)
530             lastLine = markLine;
531         if (markLine < editorLine && prevLine < markLine)
532             prevLine = markLine;
533         if (markLine > editorLine &&
534             (nextLine == -1 || nextLine > markLine))
535             nextLine = markLine;
536     }
537 
538     EditorManager::addCurrentPositionToNavigationHistory();
539     if (next) {
540         if (nextLine == -1)
541             editor->gotoLine(firstLine);
542         else
543             editor->gotoLine(nextLine);
544     } else {
545         if (prevLine == -1)
546             editor->gotoLine(lastLine);
547         else
548             editor->gotoLine(prevLine);
549     }
550 }
551 
next()552 void BookmarkManager::next()
553 {
554     QModelIndex current = selectionModel()->currentIndex();
555     if (!current.isValid())
556         return;
557     int row = current.row();
558     ++row;
559     while (true) {
560         if (row == m_bookmarksList.size())
561             row = 0;
562 
563         Bookmark *bk = m_bookmarksList.at(row);
564         if (gotoBookmark(bk)) {
565             QModelIndex newIndex = current.sibling(row, current.column());
566             selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::Select | QItemSelectionModel::Clear);
567             return;
568         }
569         deleteBookmark(bk);
570         if (m_bookmarksList.isEmpty()) // No bookmarks anymore ...
571             return;
572     }
573 }
574 
prev()575 void BookmarkManager::prev()
576 {
577     QModelIndex current = selectionModel()->currentIndex();
578     if (!current.isValid())
579         return;
580     if (!isAtCurrentBookmark() && gotoBookmark(bookmarkForIndex(current)))
581         return;
582     int row = current.row();
583     while (true) {
584         if (row == 0)
585             row = m_bookmarksList.size();
586         --row;
587         Bookmark *bk = m_bookmarksList.at(row);
588         if (gotoBookmark(bk)) {
589             QModelIndex newIndex = current.sibling(row, current.column());
590             selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::Select | QItemSelectionModel::Clear);
591             return;
592         }
593         deleteBookmark(bk);
594         if (m_bookmarksList.isEmpty())
595             return;
596     }
597 }
598 
state() const599 BookmarkManager::State BookmarkManager::state() const
600 {
601     if (m_bookmarksList.empty())
602         return NoBookMarks;
603 
604     IEditor *editor = EditorManager::currentEditor();
605     if (!editor)
606         return HasBookMarks;
607 
608     return m_bookmarksMap.value(editor->document()->filePath()).isEmpty() ? HasBookMarks
609                                                                           : HasBookmarksInDocument;
610 }
611 
updateActionStatus()612 void BookmarkManager::updateActionStatus()
613 {
614     IEditor *editor = EditorManager::currentEditor();
615     const bool enableToggle = editor && !editor->document()->isTemporary();
616 
617     emit updateActions(enableToggle, state());
618 }
619 
moveUp()620 void BookmarkManager::moveUp()
621 {
622     QModelIndex current = selectionModel()->currentIndex();
623     int row = current.row();
624     if (row == 0)
625         row = m_bookmarksList.size();
626      --row;
627 
628     // swap current.row() and row
629 
630     Bookmark *b = m_bookmarksList.at(row);
631     m_bookmarksList[row] = m_bookmarksList.at(current.row());
632     m_bookmarksList[current.row()] = b;
633 
634     QModelIndex topLeft = current.sibling(row, 0);
635     QModelIndex bottomRight = current.sibling(current.row(), 2);
636     emit dataChanged(topLeft, bottomRight);
637     selectionModel()->setCurrentIndex(current.sibling(row, 0), QItemSelectionModel::Select | QItemSelectionModel::Clear);
638 
639     saveBookmarks();
640 }
641 
moveDown()642 void BookmarkManager::moveDown()
643 {
644     QModelIndex current = selectionModel()->currentIndex();
645     int row = current.row();
646     ++row;
647     if (row == m_bookmarksList.size())
648         row = 0;
649 
650     // swap current.row() and row
651     Bookmark *b = m_bookmarksList.at(row);
652     m_bookmarksList[row] = m_bookmarksList.at(current.row());
653     m_bookmarksList[current.row()] = b;
654 
655     QModelIndex topLeft = current.sibling(current.row(), 0);
656     QModelIndex bottomRight = current.sibling(row, 2);
657     emit dataChanged(topLeft, bottomRight);
658     selectionModel()->setCurrentIndex(current.sibling(row, 0), QItemSelectionModel::Select | QItemSelectionModel::Clear);
659 
660     saveBookmarks();
661 }
662 
editByFileAndLine(const FilePath & fileName,int lineNumber)663 void BookmarkManager::editByFileAndLine(const FilePath &fileName, int lineNumber)
664 {
665     Bookmark *b = findBookmark(fileName, lineNumber);
666     QModelIndex current = selectionModel()->currentIndex();
667     selectionModel()->setCurrentIndex(current.sibling(m_bookmarksList.indexOf(b), 0),
668                                       QItemSelectionModel::Select | QItemSelectionModel::Clear);
669 
670     edit();
671 }
672 
edit()673 void BookmarkManager::edit()
674 {
675     QModelIndex current = selectionModel()->currentIndex();
676     Bookmark *b = m_bookmarksList.at(current.row());
677 
678     QDialog dlg;
679     dlg.setWindowTitle(tr("Edit Bookmark"));
680     auto layout = new QFormLayout(&dlg);
681     auto noteEdit = new QLineEdit(b->note());
682     noteEdit->setMinimumWidth(300);
683     auto lineNumberSpinbox = new QSpinBox;
684     lineNumberSpinbox->setRange(1, INT_MAX);
685     lineNumberSpinbox->setValue(b->lineNumber());
686     lineNumberSpinbox->setMaximumWidth(100);
687     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
688     connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
689     connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
690     layout->addRow(tr("Note text:"), noteEdit);
691     layout->addRow(tr("Line number:"), lineNumberSpinbox);
692     layout->addWidget(buttonBox);
693     if (dlg.exec() == QDialog::Accepted) {
694         b->move(lineNumberSpinbox->value());
695         b->updateNote(noteEdit->text().replace(QLatin1Char('\t'), QLatin1Char(' ')));
696         emit dataChanged(current, current);
697         saveBookmarks();
698     }
699 }
700 
701 /* Returns the bookmark at the given file and line number, or 0 if no such bookmark exists. */
findBookmark(const FilePath & filePath,int lineNumber)702 Bookmark *BookmarkManager::findBookmark(const FilePath &filePath, int lineNumber)
703 {
704     return Utils::findOrDefault(m_bookmarksMap.value(filePath),
705                                 Utils::equal(&Bookmark::lineNumber, lineNumber));
706 }
707 
insertBookmark(int idx,Bookmark * bookmark,bool userset)708 void BookmarkManager::insertBookmark(int idx, Bookmark *bookmark, bool userset)
709 {
710     idx = qBound(0, idx, m_bookmarksList.size());
711     beginInsertRows(QModelIndex(), idx, idx);
712 
713     m_bookmarksMap[bookmark->fileName()].append(bookmark);
714     m_bookmarksList.insert(idx, bookmark);
715 
716     endInsertRows();
717     if (userset) {
718         updateActionStatus();
719         saveBookmarks();
720     }
721     selectionModel()->setCurrentIndex(index(idx, 0, QModelIndex()),
722                                       QItemSelectionModel::Select | QItemSelectionModel::Clear);
723 }
724 
725 /* Adds a bookmark to the internal data structures. The 'userset' parameter
726  * determines whether action status should be updated and whether the bookmarks
727  * should be saved to the session settings.
728  */
addBookmark(Bookmark * bookmark,bool userset)729 void BookmarkManager::addBookmark(Bookmark *bookmark, bool userset)
730 {
731     insertBookmark(m_bookmarksList.size(), bookmark, userset);
732 }
733 
734 /* Adds a new bookmark based on information parsed from the string. */
addBookmark(const QString & s)735 void BookmarkManager::addBookmark(const QString &s)
736 {
737     // index3 is a frontier beetween note text and other bookmarks data
738     int index3 = s.lastIndexOf(QLatin1Char('\t'));
739     if (index3 < 0)
740         index3 = s.size();
741     int index2 = s.lastIndexOf(QLatin1Char(':'), index3 - 1);
742     int index1 = s.indexOf(QLatin1Char(':'));
743 
744     if (index3 != -1 || index2 != -1 || index1 != -1) {
745         const QString &filePath = s.mid(index1+1, index2-index1-1);
746         const QString &note = s.mid(index3 + 1);
747         const int lineNumber = s.mid(index2 + 1, index3 - index2 - 1).toInt();
748         if (!filePath.isEmpty() && !findBookmark(FilePath::fromString(filePath), lineNumber)) {
749             auto b = new Bookmark(lineNumber, this);
750             b->updateFileName(FilePath::fromString(filePath));
751             b->setNote(note);
752             addBookmark(b, false);
753         }
754     } else {
755         qDebug() << "BookmarkManager::addBookmark() Invalid bookmark string:" << s;
756     }
757 }
758 
759 /* Puts the bookmark in a string for storing it in the settings. */
bookmarkToString(const Bookmark * b)760 QString BookmarkManager::bookmarkToString(const Bookmark *b)
761 {
762     const QLatin1Char colon(':');
763     // Using \t as delimiter because any another symbol can be a part of note.
764     const QLatin1Char noteDelimiter('\t');
765     return colon + b->fileName().toString() +
766             colon + QString::number(b->lineNumber()) +
767             noteDelimiter + b->note();
768 }
769 
770 /* Saves the bookmarks to the session settings. */
saveBookmarks()771 void BookmarkManager::saveBookmarks()
772 {
773     QStringList list;
774     foreach (const Bookmark *bookmark, m_bookmarksList)
775         list << bookmarkToString(bookmark);
776 
777     SessionManager::setValue(QLatin1String("Bookmarks"), list);
778 }
779 
780 /* Loads the bookmarks from the session settings. */
loadBookmarks()781 void BookmarkManager::loadBookmarks()
782 {
783     removeAllBookmarks();
784     const QStringList &list = SessionManager::value(QLatin1String("Bookmarks")).toStringList();
785     foreach (const QString &bookmarkString, list)
786         addBookmark(bookmarkString);
787 
788     updateActionStatus();
789 }
790 
isAtCurrentBookmark() const791 bool BookmarkManager::isAtCurrentBookmark() const
792 {
793     Bookmark *bk = bookmarkForIndex(selectionModel()->currentIndex());
794     if (!bk)
795         return true;
796     IEditor *currentEditor = EditorManager::currentEditor();
797     return currentEditor
798            && currentEditor->document()->filePath() == bk->fileName()
799            && currentEditor->currentLine() == bk->lineNumber();
800 }
801 
802 // BookmarkViewFactory
803 
BookmarkViewFactory(BookmarkManager * bm)804 BookmarkViewFactory::BookmarkViewFactory(BookmarkManager *bm)
805     : m_manager(bm)
806 {
807     setDisplayName(BookmarkView::tr("Bookmarks"));
808     setPriority(300);
809     setId("Bookmarks");
810     setActivationSequence(QKeySequence(useMacShortcuts ? tr("Alt+Meta+M") : tr("Alt+M")));
811 }
812 
createWidget()813 NavigationView BookmarkViewFactory::createWidget()
814 {
815     auto view = new BookmarkView(m_manager);
816     return {view, view->createToolBarWidgets()};
817 }
818 
819 } // namespace Internal
820 } // namespace Bookmarks
821