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 &parameters, 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 &parameters, 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