1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2013 - 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19
20 #include "HistoryContentsWidget.h"
21 #include "../../../core/Application.h"
22 #include "../../../core/ThemesManager.h"
23 #include "../../../core/Utils.h"
24 #include "../../../ui/Action.h"
25 #include "../../../ui/MainWindow.h"
26
27 #include "ui_HistoryContentsWidget.h"
28
29 #include <QtCore/QTimer>
30 #include <QtGui/QClipboard>
31 #include <QtGui/QMouseEvent>
32 #include <QtWidgets/QMenu>
33
34 namespace Otter
35 {
36
HistoryContentsWidget(const QVariantMap & parameters,Window * window,QWidget * parent)37 HistoryContentsWidget::HistoryContentsWidget(const QVariantMap ¶meters, Window *window, QWidget *parent) : ContentsWidget(parameters, window, parent),
38 m_model(new QStandardItemModel(this)),
39 m_isLoading(true),
40 m_ui(new Ui::HistoryContentsWidget)
41 {
42 m_ui->setupUi(this);
43 m_ui->filterLineEditWidget->setClearOnEscape(true);
44
45 const QStringList groups({tr("Today"), tr("Yesterday"), tr("Earlier This Week"), tr("Previous Week"), tr("Earlier This Month"), tr("Earlier This Year"), tr("Older")});
46
47 for (int i = 0; i < groups.count(); ++i)
48 {
49 m_model->appendRow(new QStandardItem(ThemesManager::createIcon(QLatin1String("inode-directory")), groups.at(i)));
50 }
51
52 m_model->setHorizontalHeaderLabels({tr("Address"), tr("Title"), tr("Date")});
53 m_model->setHeaderData(0, Qt::Horizontal, 300, HeaderViewWidget::WidthRole);
54 m_model->setHeaderData(1, Qt::Horizontal, 300, HeaderViewWidget::WidthRole);
55 m_model->setSortRole(Qt::DisplayRole);
56
57 m_ui->historyViewWidget->setViewMode(ItemViewWidget::TreeView);
58 m_ui->historyViewWidget->setModel(m_model, true);
59 m_ui->historyViewWidget->setSortRoleMapping({{2, TimeVisitedRole}});
60 m_ui->historyViewWidget->installEventFilter(this);
61 m_ui->historyViewWidget->viewport()->installEventFilter(this);
62
63 for (int i = 0; i < m_model->rowCount(); ++i)
64 {
65 m_ui->historyViewWidget->setRowHidden(i, m_model->invisibleRootItem()->index(), true);
66 }
67
68 QTimer::singleShot(100, this, &HistoryContentsWidget::populateEntries);
69
70 connect(HistoryManager::getBrowsingHistoryModel(), &HistoryModel::cleared, this, &HistoryContentsWidget::populateEntries);
71 connect(HistoryManager::getBrowsingHistoryModel(), &HistoryModel::entryAdded, this, &HistoryContentsWidget::handleEntryAdded);
72 connect(HistoryManager::getBrowsingHistoryModel(), &HistoryModel::entryModified, this, &HistoryContentsWidget::handleEntryModified);
73 connect(HistoryManager::getBrowsingHistoryModel(), &HistoryModel::entryRemoved, this, &HistoryContentsWidget::handleEntryRemoved);
74 connect(HistoryManager::getInstance(), &HistoryManager::dayChanged, this, &HistoryContentsWidget::populateEntries);
75 connect(m_ui->filterLineEditWidget, &LineEditWidget::textChanged, m_ui->historyViewWidget, &ItemViewWidget::setFilterString);
76 connect(m_ui->historyViewWidget, &ItemViewWidget::doubleClicked, this, &HistoryContentsWidget::openEntry);
77 connect(m_ui->historyViewWidget, &ItemViewWidget::customContextMenuRequested, this, &HistoryContentsWidget::showContextMenu);
78 }
79
~HistoryContentsWidget()80 HistoryContentsWidget::~HistoryContentsWidget()
81 {
82 delete m_ui;
83 }
84
changeEvent(QEvent * event)85 void HistoryContentsWidget::changeEvent(QEvent *event)
86 {
87 ContentsWidget::changeEvent(event);
88
89 if (event->type() == QEvent::LanguageChange)
90 {
91 m_ui->retranslateUi(this);
92
93 m_model->setHorizontalHeaderLabels({tr("Address"), tr("Title"), tr("Date")});
94 }
95 }
96
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)97 void HistoryContentsWidget::triggerAction(int identifier, const QVariantMap ¶meters, ActionsManager::TriggerType trigger)
98 {
99 switch (identifier)
100 {
101 case ActionsManager::FindAction:
102 case ActionsManager::QuickFindAction:
103 m_ui->filterLineEditWidget->setFocus();
104
105 break;
106 case ActionsManager::ActivateContentAction:
107 m_ui->historyViewWidget->setFocus();
108
109 break;
110 default:
111 ContentsWidget::triggerAction(identifier, parameters, trigger);
112
113 break;
114 }
115 }
116
print(QPrinter * printer)117 void HistoryContentsWidget::print(QPrinter *printer)
118 {
119 m_ui->historyViewWidget->render(printer);
120 }
121
populateEntries()122 void HistoryContentsWidget::populateEntries()
123 {
124 const QDate date(QDate::currentDate());
125 const QVector<QDate> dates({date, date.addDays(-1), date.addDays(-7), date.addDays(-14), date.addDays(-30), date.addDays(-365)});
126
127 for (int i = 0; i < m_model->rowCount(); ++i)
128 {
129 QStandardItem *groupItem(m_model->item(i, 0));
130
131 if (groupItem)
132 {
133 groupItem->setData(dates.value(i, QDate()), GroupDateRole);
134 groupItem->removeRows(0, groupItem->rowCount());
135 }
136 }
137
138 const HistoryModel *model(HistoryManager::getBrowsingHistoryModel());
139
140 for (int i = 0; i < model->rowCount(); ++i)
141 {
142 handleEntryAdded(static_cast<HistoryModel::Entry*>(model->item(i, 0)));
143 }
144
145 const QString expandBranches(SettingsManager::getOption(SettingsManager::History_ExpandBranchesOption).toString());
146
147 if (expandBranches == QLatin1String("first"))
148 {
149 for (int i = 0; i < m_model->rowCount(); ++i)
150 {
151 const QModelIndex index(m_model->index(i, 0));
152
153 if (m_model->rowCount(index) > 0)
154 {
155 m_ui->historyViewWidget->expand(m_ui->historyViewWidget->getProxyModel()->mapFromSource(index));
156
157 break;
158 }
159 }
160 }
161 else if (expandBranches == QLatin1String("all"))
162 {
163 m_ui->historyViewWidget->expandAll();
164 }
165
166 m_isLoading = false;
167
168 emit loadingStateChanged(WebWidget::FinishedLoadingState);
169 }
170
removeEntry()171 void HistoryContentsWidget::removeEntry()
172 {
173 const quint64 entry(getEntry(m_ui->historyViewWidget->currentIndex()));
174
175 if (entry > 0)
176 {
177 HistoryManager::removeEntry(entry);
178 }
179 }
180
removeDomainEntries()181 void HistoryContentsWidget::removeDomainEntries()
182 {
183 const QStandardItem *domainItem(findEntry(getEntry(m_ui->historyViewWidget->currentIndex())));
184
185 if (!domainItem)
186 {
187 return;
188 }
189
190 const QString host(QUrl(domainItem->text()).host());
191 QVector<quint64> entries;
192
193 for (int i = 0; i < m_model->rowCount(); ++i)
194 {
195 const QStandardItem *groupItem(m_model->item(i, 0));
196
197 if (!groupItem)
198 {
199 continue;
200 }
201
202 for (int j = (groupItem->rowCount() - 1); j >= 0; --j)
203 {
204 const QStandardItem *entryItem(groupItem->child(j, 0));
205
206 if (entryItem && host == QUrl(entryItem->text()).host())
207 {
208 entries.append(entryItem->data(IdentifierRole).toULongLong());
209 }
210 }
211 }
212
213 HistoryManager::removeEntries(entries);
214 }
215
openEntry()216 void HistoryContentsWidget::openEntry()
217 {
218 const QModelIndex index(m_ui->historyViewWidget->currentIndex());
219
220 if (!index.isValid() || index.parent() == m_model->invisibleRootItem()->index())
221 {
222 return;
223 }
224
225 const QUrl url(index.sibling(index.row(), 0).data(Qt::DisplayRole).toString());
226
227 if (url.isValid())
228 {
229 const QAction *action(qobject_cast<QAction*>(sender()));
230 MainWindow *mainWindow(MainWindow::findMainWindow(this));
231
232 if (mainWindow)
233 {
234 mainWindow->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), url}, {QLatin1String("hints"), QVariant(action ? static_cast<SessionsManager::OpenHints>(action->data().toInt()) : SessionsManager::DefaultOpen)}});
235 }
236 }
237 }
238
bookmarkEntry()239 void HistoryContentsWidget::bookmarkEntry()
240 {
241 const QStandardItem *entryItem(findEntry(getEntry(m_ui->historyViewWidget->currentIndex())));
242
243 if (entryItem)
244 {
245 Application::triggerAction(ActionsManager::BookmarkPageAction, {{QLatin1String("url"), entryItem->text()}, {QLatin1String("title"), m_ui->historyViewWidget->currentIndex().sibling(m_ui->historyViewWidget->currentIndex().row(), 1).data(Qt::DisplayRole).toString()}}, parentWidget());
246 }
247 }
248
copyEntryLink()249 void HistoryContentsWidget::copyEntryLink()
250 {
251 const QStandardItem *entryItem(findEntry(getEntry(m_ui->historyViewWidget->currentIndex())));
252
253 if (entryItem)
254 {
255 QApplication::clipboard()->setText(entryItem->text());
256 }
257 }
258
handleEntryAdded(HistoryModel::Entry * entry)259 void HistoryContentsWidget::handleEntryAdded(HistoryModel::Entry *entry)
260 {
261 if (!entry || entry->getIdentifier() == 0 || findEntry(entry->getIdentifier()))
262 {
263 return;
264 }
265
266 QStandardItem *groupItem(nullptr);
267
268 for (int i = 0; i < m_model->rowCount(); ++i)
269 {
270 groupItem = m_model->item(i, 0);
271
272 const QDate date(groupItem ? groupItem->data(GroupDateRole).toDate() : QDate());
273
274 if (!date.isValid() || entry->getTimeVisited().date() >= date)
275 {
276 break;
277 }
278
279 groupItem = nullptr;
280 }
281
282 if (!groupItem)
283 {
284 return;
285 }
286
287 QList<QStandardItem*> entryItems({new QStandardItem(entry->getIcon(), entry->getUrl().toDisplayString().replace(QLatin1String("%23"), QString(QLatin1Char('#')))), new QStandardItem(entry->getTitle()), new QStandardItem(Utils::formatDateTime(entry->getTimeVisited()))});
288 entryItems[0]->setData(entry->getIdentifier(), IdentifierRole);
289 entryItems[0]->setFlags(entryItems[0]->flags() | Qt::ItemNeverHasChildren);
290 entryItems[1]->setFlags(entryItems[1]->flags() | Qt::ItemNeverHasChildren);
291 entryItems[2]->setData(entry->getTimeVisited(), TimeVisitedRole);
292 entryItems[2]->setFlags(entryItems[2]->flags() | Qt::ItemNeverHasChildren);
293 entryItems[2]->setToolTip(Utils::formatDateTime(entry->getTimeVisited(), {}, false));
294
295 groupItem->appendRow(entryItems);
296
297 m_ui->historyViewWidget->setRowHidden(groupItem->row(), groupItem->index().parent(), false);
298
299 if (sender() && groupItem->rowCount() == 1 && SettingsManager::getOption(SettingsManager::History_ExpandBranchesOption).toString() == QLatin1String("first"))
300 {
301 for (int i = 0; i < m_model->rowCount(); ++i)
302 {
303 const QModelIndex index(m_model->index(i, 0));
304
305 if (m_model->rowCount(index) > 0)
306 {
307 m_ui->historyViewWidget->expand(m_ui->historyViewWidget->getProxyModel()->mapFromSource(index));
308
309 break;
310 }
311 }
312 }
313 }
314
handleEntryModified(HistoryModel::Entry * entry)315 void HistoryContentsWidget::handleEntryModified(HistoryModel::Entry *entry)
316 {
317 if (!entry || entry->getIdentifier() == 0)
318 {
319 return;
320 }
321
322 QStandardItem *entryItem(findEntry(entry->getIdentifier()));
323
324 if (!entryItem)
325 {
326 handleEntryAdded(entry);
327
328 return;
329 }
330
331 entryItem->setIcon(entry->getIcon());
332 entryItem->setText(entry->getUrl().toDisplayString());
333 entryItem->parent()->child(entryItem->row(), 1)->setText(entry->getTitle());
334 entryItem->parent()->child(entryItem->row(), 2)->setText(Utils::formatDateTime(entry->getTimeVisited()));
335 }
336
handleEntryRemoved(HistoryModel::Entry * entry)337 void HistoryContentsWidget::handleEntryRemoved(HistoryModel::Entry *entry)
338 {
339 if (!entry || entry->getIdentifier() == 0)
340 {
341 return;
342 }
343
344 QStandardItem *entryItem(findEntry(entry->getIdentifier()));
345
346 if (entryItem)
347 {
348 QStandardItem *groupItem(entryItem->parent());
349
350 if (groupItem)
351 {
352 m_model->removeRow(entryItem->row(), groupItem->index());
353
354 if (groupItem->rowCount() == 0)
355 {
356 m_ui->historyViewWidget->setRowHidden(groupItem->row(), m_model->invisibleRootItem()->index(), true);
357 }
358 }
359 }
360 }
361
showContextMenu(const QPoint & position)362 void HistoryContentsWidget::showContextMenu(const QPoint &position)
363 {
364 MainWindow *mainWindow(MainWindow::findMainWindow(this));
365 const quint64 entry(getEntry(m_ui->historyViewWidget->indexAt(position)));
366 QMenu menu(this);
367
368 if (entry > 0)
369 {
370 menu.addAction(ThemesManager::createIcon(QLatin1String("document-open")), QCoreApplication::translate("actions", "Open"), this, &HistoryContentsWidget::openEntry);
371 menu.addAction(QCoreApplication::translate("actions", "Open in New Tab"), this, &HistoryContentsWidget::openEntry)->setData(SessionsManager::NewTabOpen);
372 menu.addAction(QCoreApplication::translate("actions", "Open in New Background Tab"), this, &HistoryContentsWidget::openEntry)->setData(static_cast<int>(SessionsManager::NewTabOpen | SessionsManager::BackgroundOpen));
373 menu.addSeparator();
374 menu.addAction(QCoreApplication::translate("actions", "Open in New Window"), this, &HistoryContentsWidget::openEntry)->setData(SessionsManager::NewWindowOpen);
375 menu.addAction(QCoreApplication::translate("actions", "Open in New Background Window"), this, &HistoryContentsWidget::openEntry)->setData(static_cast<int>(SessionsManager::NewWindowOpen | SessionsManager::BackgroundOpen));
376 menu.addSeparator();
377 menu.addAction(tr("Add to Bookmarks…"), this, &HistoryContentsWidget::bookmarkEntry);
378 menu.addAction(tr("Copy Link to Clipboard"), this, &HistoryContentsWidget::copyEntryLink);
379 menu.addSeparator();
380 menu.addAction(tr("Remove Entry"), this, &HistoryContentsWidget::removeEntry);
381 menu.addAction(tr("Remove All Entries from This Domain"), this, &HistoryContentsWidget::removeDomainEntries);
382 }
383
384 menu.addAction(new Action(ActionsManager::ClearHistoryAction, {}, ActionExecutor::Object(mainWindow, mainWindow), &menu));
385 menu.exec(m_ui->historyViewWidget->mapToGlobal(position));
386 }
387
findEntry(quint64 identifier)388 QStandardItem* HistoryContentsWidget::findEntry(quint64 identifier)
389 {
390 for (int i = 0; i < m_model->rowCount(); ++i)
391 {
392 const QStandardItem *groupItem(m_model->item(i, 0));
393
394 if (groupItem)
395 {
396 for (int j = 0; j < groupItem->rowCount(); ++j)
397 {
398 QStandardItem *entryItem(groupItem->child(j, 0));
399
400 if (entryItem && entryItem->data(IdentifierRole).toULongLong() == identifier)
401 {
402 return entryItem;
403 }
404 }
405 }
406 }
407
408 return nullptr;
409 }
410
getTitle() const411 QString HistoryContentsWidget::getTitle() const
412 {
413 return tr("History");
414 }
415
getType() const416 QLatin1String HistoryContentsWidget::getType() const
417 {
418 return QLatin1String("history");
419 }
420
getUrl() const421 QUrl HistoryContentsWidget::getUrl() const
422 {
423 return QUrl(QLatin1String("about:history"));
424 }
425
getIcon() const426 QIcon HistoryContentsWidget::getIcon() const
427 {
428 return ThemesManager::createIcon(QLatin1String("view-history"), false);
429 }
430
getLoadingState() const431 WebWidget::LoadingState HistoryContentsWidget::getLoadingState() const
432 {
433 return (m_isLoading ? WebWidget::OngoingLoadingState : WebWidget::FinishedLoadingState);
434 }
435
getEntry(const QModelIndex & index) const436 quint64 HistoryContentsWidget::getEntry(const QModelIndex &index) const
437 {
438 return ((index.isValid() && index.parent().isValid() && index.parent().parent() == m_model->invisibleRootItem()->index()) ? index.sibling(index.row(), 0).data(IdentifierRole).toULongLong() : 0);
439 }
440
eventFilter(QObject * object,QEvent * event)441 bool HistoryContentsWidget::eventFilter(QObject *object, QEvent *event)
442 {
443 if (object == m_ui->historyViewWidget && event->type() == QEvent::KeyPress)
444 {
445 const QKeyEvent *keyEvent(static_cast<QKeyEvent*>(event));
446
447 switch (keyEvent->key())
448 {
449 case Qt::Key_Delete:
450 removeEntry();
451
452 return true;
453 case Qt::Key_Enter:
454 case Qt::Key_Return:
455 openEntry();
456
457 return true;
458 default:
459 break;
460 }
461 }
462 else if (object == m_ui->historyViewWidget->viewport() && event->type() == QEvent::MouseButtonRelease)
463 {
464 const QMouseEvent *mouseEvent(static_cast<QMouseEvent*>(event));
465
466 if ((mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() != Qt::NoModifier) || mouseEvent->button() == Qt::MiddleButton)
467 {
468 const QModelIndex entryIndex(m_ui->historyViewWidget->currentIndex());
469
470 if (!entryIndex.isValid() || entryIndex.parent() == m_model->invisibleRootItem()->index())
471 {
472 return ContentsWidget::eventFilter(object, event);
473 }
474
475 MainWindow *mainWindow(MainWindow::findMainWindow(this));
476 const QUrl url(entryIndex.sibling(entryIndex.row(), 0).data(Qt::DisplayRole).toString());
477
478 if (mainWindow && url.isValid())
479 {
480 mainWindow->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), url}, {QLatin1String("hints"), QVariant(SessionsManager::calculateOpenHints(SessionsManager::NewTabOpen, mouseEvent->button(), mouseEvent->modifiers()))}});
481
482 return true;
483 }
484 }
485 }
486
487 return ContentsWidget::eventFilter(object, event);
488 }
489
490 }
491