1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Assistant of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28 #include "tracer.h"
29 
30 #include "bookmarkmanager.h"
31 #include "bookmarkmanagerwidget.h"
32 #include "bookmarkdialog.h"
33 #include "bookmarkfiltermodel.h"
34 #include "bookmarkitem.h"
35 #include "bookmarkmodel.h"
36 #include "centralwidget.h"
37 #include "helpenginewrapper.h"
38 
39 #include <QtWidgets/QMenu>
40 #include <QtGui/QKeyEvent>
41 #include <QtWidgets/QMessageBox>
42 #include <QtCore/QSortFilterProxyModel>
43 #include <QtWidgets/QToolBar>
44 
45 QT_BEGIN_NAMESPACE
46 
47 // -- BookmarkManager::BookmarkWidget
48 
focusInEvent(QFocusEvent * event)49 void BookmarkManager::BookmarkWidget::focusInEvent(QFocusEvent *event)
50 {
51     TRACE_OBJ
52     if (event->reason() != Qt::MouseFocusReason) {
53         ui.lineEdit->selectAll();
54         ui.lineEdit->setFocus();
55 
56         // force the focus in event on bookmark manager
57         emit focusInEventOccurred();
58     }
59 }
60 
61 // -- BookmarkManager::BookmarkTreeView
62 
BookmarkTreeView(QWidget * parent)63 BookmarkManager::BookmarkTreeView::BookmarkTreeView(QWidget *parent)
64     : QTreeView(parent)
65 {
66     TRACE_OBJ
67     setAcceptDrops(true);
68     setDragEnabled(true);
69     setAutoExpandDelay(1000);
70     setUniformRowHeights(true);
71     setDropIndicatorShown(true);
72     setExpandsOnDoubleClick(true);
73 
74     connect(this, &QTreeView::expanded, this, &BookmarkTreeView::setExpandedData);
75     connect(this, &QTreeView::collapsed, this, &BookmarkTreeView::setExpandedData);
76 }
77 
subclassKeyPressEvent(QKeyEvent * event)78 void BookmarkManager::BookmarkTreeView::subclassKeyPressEvent(QKeyEvent *event)
79 {
80     TRACE_OBJ
81     QTreeView::keyPressEvent(event);
82 }
83 
commitData(QWidget * editor)84 void BookmarkManager::BookmarkTreeView::commitData(QWidget *editor)
85 {
86     QTreeView::commitData(editor);
87     emit editingDone();
88 }
89 
setExpandedData(const QModelIndex & index)90 void BookmarkManager::BookmarkTreeView::setExpandedData(const QModelIndex &index)
91 {
92     TRACE_OBJ
93     if (BookmarkModel *treeModel = qobject_cast<BookmarkModel*> (model()))
94         treeModel->setData(index, isExpanded(index), UserRoleExpanded);
95 }
96 
97 // -- BookmarkManager
98 
99 QMutex BookmarkManager::mutex;
100 BookmarkManager* BookmarkManager::bookmarkManager = nullptr;
101 
102 // -- public
103 
instance()104 BookmarkManager* BookmarkManager::instance()
105 {
106     TRACE_OBJ
107     if (!bookmarkManager) {
108         QMutexLocker _(&mutex);
109         if (!bookmarkManager)
110             bookmarkManager = new BookmarkManager();
111     }
112     return bookmarkManager;
113 }
114 
destroy()115 void BookmarkManager::destroy()
116 {
117     TRACE_OBJ
118     delete bookmarkManager;
119     bookmarkManager = nullptr;
120 }
121 
bookmarkDockWidget() const122 QWidget* BookmarkManager::bookmarkDockWidget() const
123 {
124     TRACE_OBJ
125     if (bookmarkWidget)
126         return bookmarkWidget;
127     return nullptr;
128 }
129 
setBookmarksMenu(QMenu * menu)130 void BookmarkManager::setBookmarksMenu(QMenu* menu)
131 {
132     TRACE_OBJ
133     bookmarkMenu = menu;
134     refreshBookmarkMenu();
135 }
136 
setBookmarksToolbar(QToolBar * toolBar)137 void BookmarkManager::setBookmarksToolbar(QToolBar *toolBar)
138 {
139     TRACE_OBJ
140     m_toolBar = toolBar;
141     refreshBookmarkToolBar();
142 }
143 
144 // -- public slots
145 
addBookmark(const QString & title,const QString & url)146 void BookmarkManager::addBookmark(const QString &title, const QString &url)
147 {
148     TRACE_OBJ
149     showBookmarkDialog(title.isEmpty() ? tr("Untitled") : title,
150         url.isEmpty() ? QLatin1String("about:blank") : url);
151 
152     storeBookmarks();
153 }
154 
155 // -- private
156 
BookmarkManager()157 BookmarkManager::BookmarkManager()
158     : bookmarkModel(new BookmarkModel)
159     , bookmarkWidget(new BookmarkWidget)
160     , bookmarkTreeView(new BookmarkTreeView)
161 {
162     TRACE_OBJ
163     bookmarkWidget->installEventFilter(this);
164     connect(bookmarkWidget->ui.add, &QAbstractButton::clicked,
165             this, &BookmarkManager::addBookmarkActivated);
166     connect(bookmarkWidget->ui.remove, &QAbstractButton::clicked,
167             this, &BookmarkManager::removeBookmarkActivated);
168     connect(bookmarkWidget->ui.lineEdit, &QLineEdit::textChanged,
169             this, &BookmarkManager::textChanged);
170     connect(bookmarkWidget, &BookmarkWidget::focusInEventOccurred,
171             this, &BookmarkManager::focusInEventOccurred);
172 
173     bookmarkTreeView->setModel(bookmarkModel);
174     bookmarkTreeView->installEventFilter(this);
175     bookmarkTreeView->viewport()->installEventFilter(this);
176     bookmarkTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
177     bookmarkWidget->ui.stackedWidget->addWidget(bookmarkTreeView);
178 
179     connect(bookmarkTreeView, &QAbstractItemView::activated,
180             this, [this](const QModelIndex &index) { setSourceFromIndex(index, false); });
181     connect(bookmarkTreeView, &QWidget::customContextMenuRequested,
182             this, &BookmarkManager::customContextMenuRequested);
183     connect(bookmarkTreeView, &BookmarkTreeView::editingDone,
184             this, &BookmarkManager::storeBookmarks);
185 
186     connect(&HelpEngineWrapper::instance(), &HelpEngineWrapper::setupFinished,
187             this, &BookmarkManager::setupFinished);
188 
189     connect(bookmarkModel, &QAbstractItemModel::rowsRemoved,
190             this, &BookmarkManager::refreshBookmarkMenu);
191     connect(bookmarkModel, &QAbstractItemModel::rowsInserted,
192             this, &BookmarkManager::refreshBookmarkMenu);
193     connect(bookmarkModel, &QAbstractItemModel::dataChanged,
194             this, &BookmarkManager::refreshBookmarkMenu);
195 
196     connect(bookmarkModel, &QAbstractItemModel::rowsRemoved,
197             this, &BookmarkManager::refreshBookmarkToolBar);
198     connect(bookmarkModel, &QAbstractItemModel::rowsInserted,
199             this, &BookmarkManager::refreshBookmarkToolBar);
200     connect(bookmarkModel, &QAbstractItemModel::dataChanged,
201             this, &BookmarkManager::refreshBookmarkToolBar);
202 
203 }
204 
~BookmarkManager()205 BookmarkManager::~BookmarkManager()
206 {
207     TRACE_OBJ
208     delete bookmarkManagerWidget;
209     storeBookmarks();
210     delete bookmarkModel;
211 }
212 
removeItem(const QModelIndex & index)213 void BookmarkManager::removeItem(const QModelIndex &index)
214 {
215     TRACE_OBJ
216     QModelIndex current = index;
217     if (typeAndSearch) { // need to map because of proxy
218         current = typeAndSearchModel->mapToSource(current);
219         current = bookmarkFilterModel->mapToSource(current);
220     } else if (!bookmarkModel->parent(index).isValid()) {
221         return;  // check if we should delete the "Bookmarks Menu", bail
222     }
223 
224     if (bookmarkModel->hasChildren(current)) {
225         int value = QMessageBox::question(bookmarkTreeView, tr("Remove"),
226             tr("You are going to delete a Folder, this will also<br>"
227             "remove it's content. Are you sure to continue?"),
228             QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
229         if (value == QMessageBox::Cancel)
230             return;
231     }
232     bookmarkModel->removeItem(current);
233 
234     storeBookmarks();
235 }
236 
eventFilter(QObject * object,QEvent * event)237 bool BookmarkManager::eventFilter(QObject *object, QEvent *event)
238 {
239     if (object != bookmarkTreeView && object != bookmarkTreeView->viewport()
240         && object != bookmarkWidget)
241             return QObject::eventFilter(object, event);
242 
243     TRACE_OBJ
244     const bool isWidget = object == bookmarkWidget;
245     if (event->type() == QEvent::KeyPress) {
246         QKeyEvent *ke = static_cast<QKeyEvent*>(event);
247         switch (ke->key()) {
248             case Qt::Key_F2:
249                 renameBookmark(bookmarkTreeView->currentIndex());
250                 break;
251 
252             case Qt::Key_Delete:
253                 removeItem(bookmarkTreeView->currentIndex());
254                 return true;
255 
256             case Qt::Key_Up:    // needs event filter on widget
257             case Qt::Key_Down:
258                 if (isWidget)
259                     bookmarkTreeView->subclassKeyPressEvent(ke);
260                 break;
261 
262             case Qt::Key_Escape:
263                 emit escapePressed();
264                 break;
265 
266             default: break;
267         }
268     }
269 
270     if (event->type() == QEvent::MouseButtonRelease && !isWidget) {
271         QMouseEvent *me = static_cast<QMouseEvent*>(event);
272         switch (me->button()) {
273             case Qt::LeftButton:
274                 if (me->modifiers() & Qt::ControlModifier)
275                     setSourceFromIndex(bookmarkTreeView->currentIndex(), true);
276                 break;
277 
278             case Qt::MiddleButton:
279                 setSourceFromIndex(bookmarkTreeView->currentIndex(), true);
280                 break;
281 
282             default: break;
283         }
284     }
285 
286     return QObject::eventFilter(object, event);
287 }
288 
buildBookmarksMenu(const QModelIndex & index,QMenu * menu)289 void BookmarkManager::buildBookmarksMenu(const QModelIndex &index, QMenu* menu)
290 {
291     TRACE_OBJ
292     if (!index.isValid())
293         return;
294 
295     const QString &text = index.data().toString();
296     const QIcon &icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
297     if (index.data(UserRoleFolder).toBool()) {
298         if (QMenu* subMenu = menu->addMenu(icon, text)) {
299             for (int i = 0; i < bookmarkModel->rowCount(index); ++i)
300                 buildBookmarksMenu(bookmarkModel->index(i, 0, index), subMenu);
301         }
302     } else {
303         QAction *action = menu->addAction(icon, text);
304         action->setData(index.data(UserRoleUrl).toString());
305     }
306 }
307 
showBookmarkDialog(const QString & name,const QString & url)308 void BookmarkManager::showBookmarkDialog(const QString &name, const QString &url)
309 {
310     TRACE_OBJ
311     BookmarkDialog dialog(bookmarkModel, name, url, bookmarkTreeView);
312     dialog.exec();
313 }
314 
315 // -- private slots
316 
setupFinished()317 void BookmarkManager::setupFinished()
318 {
319     TRACE_OBJ
320     bookmarkModel->setBookmarks(HelpEngineWrapper::instance().bookmarks());
321     bookmarkModel->expandFoldersIfNeeeded(bookmarkTreeView);
322 
323     refreshBookmarkMenu();
324     refreshBookmarkToolBar();
325 
326     bookmarkTreeView->hideColumn(1);
327     bookmarkTreeView->header()->setVisible(false);
328     bookmarkTreeView->header()->setStretchLastSection(true);
329 
330     if (!bookmarkFilterModel)
331         bookmarkFilterModel = new BookmarkFilterModel(this);
332     bookmarkFilterModel->setSourceModel(bookmarkModel);
333     bookmarkFilterModel->filterBookmarkFolders();
334 
335     if (!typeAndSearchModel)
336         typeAndSearchModel = new QSortFilterProxyModel(this);
337     typeAndSearchModel->setDynamicSortFilter(true);
338     typeAndSearchModel->setSourceModel(bookmarkFilterModel);
339 }
340 
storeBookmarks()341 void BookmarkManager::storeBookmarks()
342 {
343     HelpEngineWrapper::instance().setBookmarks(bookmarkModel->bookmarks());
344 }
345 
addBookmarkActivated()346 void BookmarkManager::addBookmarkActivated()
347 {
348     TRACE_OBJ
349     if (CentralWidget *widget = CentralWidget::instance())
350         addBookmark(widget->currentTitle(), widget->currentSource().toString());
351 }
352 
removeBookmarkActivated()353 void BookmarkManager::removeBookmarkActivated()
354 {
355     TRACE_OBJ
356     removeItem(bookmarkTreeView->currentIndex());
357 }
358 
manageBookmarks()359 void BookmarkManager::manageBookmarks()
360 {
361     TRACE_OBJ
362     if (bookmarkManagerWidget == nullptr) {
363         bookmarkManagerWidget = new BookmarkManagerWidget(bookmarkModel);
364         connect(bookmarkManagerWidget, &BookmarkManagerWidget::setSource,
365                 this, &BookmarkManager::setSource);
366         connect(bookmarkManagerWidget, &BookmarkManagerWidget::setSourceInNewTab,
367                 this, &BookmarkManager::setSourceInNewTab);
368         connect(bookmarkManagerWidget, &BookmarkManagerWidget::managerWidgetAboutToClose,
369                 this, &BookmarkManager::managerWidgetAboutToClose);
370     }
371     bookmarkManagerWidget->show();
372     bookmarkManagerWidget->raise();
373 }
374 
refreshBookmarkMenu()375 void BookmarkManager::refreshBookmarkMenu()
376 {
377     TRACE_OBJ
378     if (!bookmarkMenu)
379         return;
380 
381     bookmarkMenu->clear();
382 
383     bookmarkMenu->addAction(tr("Manage Bookmarks..."), this,
384                             &BookmarkManager::manageBookmarks);
385     bookmarkMenu->addAction(QIcon::fromTheme("bookmark-new"), tr("Add Bookmark..."),
386                             this, &BookmarkManager::addBookmarkActivated,
387                             QKeySequence(tr("Ctrl+D")));
388 
389     bookmarkMenu->addSeparator();
390 
391     QModelIndex root = bookmarkModel->index(0, 0, QModelIndex()).parent();
392     buildBookmarksMenu(bookmarkModel->index(0, 0, root), bookmarkMenu);
393 
394     bookmarkMenu->addSeparator();
395 
396     root = bookmarkModel->index(1, 0, QModelIndex());
397     for (int i = 0; i < bookmarkModel->rowCount(root); ++i)
398         buildBookmarksMenu(bookmarkModel->index(i, 0, root), bookmarkMenu);
399 
400     connect(bookmarkMenu, &QMenu::triggered,
401             this, &BookmarkManager::setSourceFromAction);
402 }
403 
refreshBookmarkToolBar()404 void BookmarkManager::refreshBookmarkToolBar()
405 {
406     TRACE_OBJ
407     if (!m_toolBar)
408         return;
409 
410     m_toolBar->clear();
411     m_toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
412 
413     const QModelIndex &root = bookmarkModel->index(0, 0, QModelIndex());
414     for (int i = 0; i < bookmarkModel->rowCount(root); ++i) {
415         const QModelIndex &index = bookmarkModel->index(i, 0, root);
416         if (index.data(UserRoleFolder).toBool()) {
417             QToolButton *button = new QToolButton(m_toolBar);
418             button->setPopupMode(QToolButton::InstantPopup);
419             button->setText(index.data().toString());
420             QMenu *menu = new QMenu(button);
421             for (int j = 0; j < bookmarkModel->rowCount(index); ++j)
422                 buildBookmarksMenu(bookmarkModel->index(j, 0, index), menu);
423             connect(menu, &QMenu::triggered,
424                     this, &BookmarkManager::setSourceFromAction);
425             button->setMenu(menu);
426             button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
427             button->setIcon(qvariant_cast<QIcon>(index.data(Qt::DecorationRole)));
428             QAction *a = m_toolBar->addWidget(button);
429             a->setText(index.data().toString());
430         } else {
431             QAction *action = m_toolBar->addAction(
432                 qvariant_cast<QIcon>(index.data(Qt::DecorationRole)),
433                 index.data().toString(), this, &BookmarkManager::setSourceFromAction);
434             action->setData(index.data(UserRoleUrl).toString());
435         }
436     }
437 }
438 
renameBookmark(const QModelIndex & index)439 void BookmarkManager::renameBookmark(const QModelIndex &index)
440 {
441     // check if we should rename the "Bookmarks Menu", bail
442     if (!typeAndSearch && !bookmarkModel->parent(index).isValid())
443         return;
444 
445     bookmarkModel->setItemsEditable(true);
446     bookmarkTreeView->edit(index);
447     bookmarkModel->setItemsEditable(false);
448 }
449 
450 
setSourceFromAction()451 void BookmarkManager::setSourceFromAction()
452 {
453     TRACE_OBJ
454     const QAction *action = qobject_cast<QAction*>(sender());
455     if (!action)
456         return;
457 
458     const QVariant &data = action->data();
459     if (data.canConvert<QUrl>())
460         emit setSource(data.toUrl());
461 }
462 
setSourceFromIndex(const QModelIndex & index,bool newTab)463 void BookmarkManager::setSourceFromIndex(const QModelIndex &index, bool newTab)
464 {
465     TRACE_OBJ
466     QAbstractItemModel *base = bookmarkModel;
467     if (typeAndSearch)
468         base = typeAndSearchModel;
469 
470     if (base->data(index, UserRoleFolder).toBool())
471         return;
472 
473     const QVariant &data = base->data(index, UserRoleUrl);
474     if (data.canConvert<QUrl>()) {
475         if (newTab)
476             emit setSourceInNewTab(data.toUrl());
477         else
478             emit setSource(data.toUrl());
479     }
480 }
481 
customContextMenuRequested(const QPoint & point)482 void BookmarkManager::customContextMenuRequested(const QPoint &point)
483 {
484     TRACE_OBJ
485     QModelIndex index = bookmarkTreeView->indexAt(point);
486     if (!index.isValid())
487         return;
488 
489     // check if we should open the menu on "Bookmarks Menu", bail
490     if (!typeAndSearch && !bookmarkModel->parent(index).isValid())
491         return;
492 
493     QAction *remove = nullptr;
494     QAction *rename = nullptr;
495     QAction *showItem = nullptr;
496     QAction *showItemInNewTab = nullptr;
497 
498     QMenu menu;
499     if (!typeAndSearch && bookmarkModel->data(index, UserRoleFolder).toBool()) {
500         remove = menu.addAction(tr("Delete Folder"));
501         rename = menu.addAction(tr("Rename Folder"));
502     } else {
503         showItem = menu.addAction(tr("Show Bookmark"));
504         showItemInNewTab = menu.addAction(tr("Show Bookmark in New Tab"));
505         menu.addSeparator();
506         remove = menu.addAction(tr("Delete Bookmark"));
507         rename = menu.addAction(tr("Rename Bookmark"));
508     }
509 
510     QAction *pickedAction = menu.exec(bookmarkTreeView->mapToGlobal(point));
511     if (pickedAction == rename)
512         renameBookmark(index);
513     else if (pickedAction == remove)
514         removeItem(index);
515     else if (pickedAction == showItem || pickedAction == showItemInNewTab)
516         setSourceFromIndex(index, pickedAction == showItemInNewTab);
517 }
518 
focusInEventOccurred()519 void BookmarkManager::focusInEventOccurred()
520 {
521     TRACE_OBJ
522     const QModelIndex &index = bookmarkTreeView->indexAt(QPoint(2, 2));
523     if (index.isValid())
524         bookmarkTreeView->setCurrentIndex(index);
525 }
526 
managerWidgetAboutToClose()527 void BookmarkManager::managerWidgetAboutToClose()
528 {
529     if (bookmarkManagerWidget)
530         bookmarkManagerWidget->deleteLater();
531     bookmarkManagerWidget = nullptr;
532 
533     storeBookmarks();
534 }
535 
textChanged(const QString & text)536 void BookmarkManager::textChanged(const QString &text)
537 {
538     TRACE_OBJ
539     if (!bookmarkWidget->ui.lineEdit->text().isEmpty()) {
540         if (!typeAndSearch) {
541             typeAndSearch = true;
542             bookmarkTreeView->setItemsExpandable(false);
543             bookmarkTreeView->setRootIsDecorated(false);
544             bookmarkTreeView->setModel(typeAndSearchModel);
545         }
546         typeAndSearchModel->setFilterRegExp(QRegExp(text));
547     } else {
548         typeAndSearch = false;
549         bookmarkTreeView->setModel(bookmarkModel);
550         bookmarkTreeView->setItemsExpandable(true);
551         bookmarkTreeView->setRootIsDecorated(true);
552         bookmarkModel->expandFoldersIfNeeeded(bookmarkTreeView);
553     }
554 }
555 
556 QT_END_NAMESPACE
557