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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | // -- BookmarkManager::BookmarkWidget |
23 | |
24 | void 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 | |
38 | BookmarkManager::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 | |
53 | void BookmarkManager::BookmarkTreeView::subclassKeyPressEvent(QKeyEvent *event) |
54 | { |
55 | TRACE_OBJ |
56 | QTreeView::keyPressEvent(event); |
57 | } |
58 | |
59 | void BookmarkManager::BookmarkTreeView::commitData(QWidget *editor) |
60 | { |
61 | QTreeView::commitData(editor); |
62 | emit editingDone(); |
63 | } |
64 | |
65 | void 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 | |
74 | QMutex BookmarkManager::mutex; |
75 | BookmarkManager* BookmarkManager::bookmarkManager = nullptr; |
76 | |
77 | // -- public |
78 | |
79 | BookmarkManager* 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 | |
90 | void BookmarkManager::destroy() |
91 | { |
92 | TRACE_OBJ |
93 | delete bookmarkManager; |
94 | bookmarkManager = nullptr; |
95 | } |
96 | |
97 | QWidget* BookmarkManager::bookmarkDockWidget() const |
98 | { |
99 | TRACE_OBJ |
100 | if (bookmarkWidget) |
101 | return bookmarkWidget; |
102 | return nullptr; |
103 | } |
104 | |
105 | void BookmarkManager::(QMenu* ) |
106 | { |
107 | TRACE_OBJ |
108 | bookmarkMenu = menu; |
109 | refreshBookmarkMenu(); |
110 | } |
111 | |
112 | void BookmarkManager::setBookmarksToolbar(QToolBar *toolBar) |
113 | { |
114 | TRACE_OBJ |
115 | m_toolBar = toolBar; |
116 | refreshBookmarkToolBar(); |
117 | } |
118 | |
119 | // -- public slots |
120 | |
121 | void 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 | |
132 | BookmarkManager::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 | |
180 | BookmarkManager::~BookmarkManager() |
181 | { |
182 | TRACE_OBJ |
183 | delete bookmarkManagerWidget; |
184 | storeBookmarks(); |
185 | delete bookmarkModel; |
186 | } |
187 | |
188 | void 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 | |
212 | bool 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 | |
264 | void BookmarkManager::(const QModelIndex &index, QMenu* ) |
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* = 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 | |
285 | void 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 | |
294 | void 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 | |
318 | void BookmarkManager::storeBookmarks() |
319 | { |
320 | HelpEngineWrapper::instance().setBookmarks(bookmarkModel->bookmarks()); |
321 | } |
322 | |
323 | void BookmarkManager::addBookmarkActivated() |
324 | { |
325 | TRACE_OBJ |
326 | if (CentralWidget *widget = CentralWidget::instance()) |
327 | addBookmark(title: widget->currentTitle(), url: widget->currentSource().toString()); |
328 | } |
329 | |
330 | void BookmarkManager::removeBookmarkActivated() |
331 | { |
332 | TRACE_OBJ |
333 | removeItem(index: bookmarkTreeView->currentIndex()); |
334 | } |
335 | |
336 | void 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 | |
352 | void BookmarkManager::() |
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 | |
378 | void 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 * = 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 | |
411 | void 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 | |
423 | void 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 | |
435 | void 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 | |
454 | void BookmarkManager::(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 ; |
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 | |
491 | void 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 | |
499 | void BookmarkManager::managerWidgetAboutToClose() |
500 | { |
501 | if (bookmarkManagerWidget) |
502 | bookmarkManagerWidget->deleteLater(); |
503 | bookmarkManagerWidget = nullptr; |
504 | |
505 | storeBookmarks(); |
506 | } |
507 | |
508 | void 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 | |
528 | QT_END_NAMESPACE |
529 | |