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

source code of qttools/src/assistant/assistant/bookmarkmanager.cpp