Falkon Develop
Cross-platform Qt-based web browser
historymodel.cpp
Go to the documentation of this file.
1/* ============================================================
2* Falkon - Qt web browser
3* Copyright (C) 2010-2017 David Rosca <nowrep@gmail.com>
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#include "historymodel.h"
19#include "historyitem.h"
20#include "iconprovider.h"
21#include "sqldatabase.h"
22
23#include <QApplication>
24#include <QDateTime>
25#include <QTimeZone>
26#include <QTimer>
27
28static QString dateTimeToString(const QDateTime &dateTime)
29{
30 const QDateTime current = QDateTime::currentDateTime();
31 if (current.date() == dateTime.date()) {
32 return dateTime.time().toString(QSL("h:mm"));
33 }
34
35 return dateTime.toString(QSL("d.M.yyyy h:mm"));
36}
37
39 : QAbstractItemModel(history)
40 , m_rootItem(new HistoryItem(nullptr))
41 , m_todayItem(nullptr)
42 , m_history(history)
43{
44 init();
45
46 connect(m_history, &History::resetHistory, this, &HistoryModel::resetHistory);
47 connect(m_history, &History::historyEntryAdded, this, &HistoryModel::historyEntryAdded);
48 connect(m_history, &History::historyEntryDeleted, this, &HistoryModel::historyEntryDeleted);
49 connect(m_history, &History::historyEntryEdited, this, &HistoryModel::historyEntryEdited);
50}
51
52QVariant HistoryModel::headerData(int section, Qt::Orientation orientation, int role) const
53{
54 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
55 switch (section) {
56 case 0:
57 return tr("Title");
58 case 1:
59 return tr("Address");
60 case 2:
61 return tr("Visit Date");
62 case 3:
63 return tr("Visit Count");
64 }
65 }
66
67 return QAbstractItemModel::headerData(section, orientation, role);
68}
69
70QVariant HistoryModel::data(const QModelIndex &index, int role) const
71{
73
74 if (index.row() < 0 || !item) {
75 return {};
76 }
77
78 if (item->isTopLevel()) {
79 switch (role) {
80 case IsTopLevelRole:
81 return true;
83 return item->startTimestamp();
85 return item->endTimestamp();
86 case Qt::DisplayRole:
87 case Qt::EditRole:
88 return index.column() == 0 ? item->title : QVariant();
89 case Qt::DecorationRole:
90 return index.column() == 0 ? QIcon::fromTheme(QSL("view-calendar"), QIcon(QSL(":/icons/menu/history_entry.svg"))) : QVariant();
91 }
92
93 return {};
94 }
95
96 const HistoryEntry entry = item->historyEntry;
97
98 switch (role) {
99 case IdRole:
100 return entry.id;
101 case TitleRole:
102 return entry.title;
103 case UrlRole:
104 return entry.url;
105 case UrlStringRole:
106 return entry.urlString;
107 case IconRole:
108 return item->icon();
109 case IsTopLevelRole:
110 return false;
112 return -1;
113 case TimestampEndRole:
114 return -1;
115 case Qt::ToolTipRole:
116 if (index.column() == 0) {
117 return QSL("%1\n%2").arg(entry.title, entry.urlString);
118 }
119 // fallthrough
120 case Qt::DisplayRole:
121 case Qt::EditRole:
122 switch (index.column()) {
123 case 0:
124 return entry.title;
125 case 1:
126 return entry.urlString;
127 case 2:
128 return dateTimeToString(entry.date);
129 case 3:
130 return entry.count;
131 }
132 break;
133 case Qt::DecorationRole:
134 if (index.column() == 0) {
135 return item->icon().isNull() ? IconProvider::emptyWebIcon() : item->icon();
136 }
137 }
138
139 return {};
140}
141
142bool HistoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
143{
145
146 if (index.row() < 0 || !item || item->isTopLevel()) {
147 return false;
148 }
149
150 if (role == IconRole) {
151 item->setIcon(value.value<QIcon>());
152 Q_EMIT dataChanged(index, index);
153 return true;
154 }
155
156 return false;
157}
158
159QModelIndex HistoryModel::index(int row, int column, const QModelIndex &parent) const
160{
161 if (!hasIndex(row, column, parent)) {
162 return {};
163 }
164
165 HistoryItem* parentItem = itemFromIndex(parent);
166 HistoryItem* childItem = parentItem->child(row);
167
168 return childItem ? createIndex(row, column, childItem) : QModelIndex();
169}
170
171QModelIndex HistoryModel::parent(const QModelIndex &index) const
172{
173 if (!index.isValid()) {
174 return {};
175 }
176
177 HistoryItem* childItem = itemFromIndex(index);
178 HistoryItem* parentItem = childItem->parent();
179
180 if (!parentItem || parentItem == m_rootItem) {
181 return {};
182 }
183
184 return createIndex(parentItem->row(), 0, parentItem);
185}
186
187Qt::ItemFlags HistoryModel::flags(const QModelIndex &index) const
188{
189 if (!index.isValid()) {
190 return Qt::NoItemFlags;
191 }
192
193 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
194}
195
196int HistoryModel::rowCount(const QModelIndex &parent) const
197{
198 if (parent.column() > 0) {
199 return 0;
200 }
201
202 HistoryItem* parentItem = itemFromIndex(parent);
203
204 return parentItem->childCount();
205}
206
207int HistoryModel::columnCount(const QModelIndex &parent) const
208{
209 Q_UNUSED(parent)
210
211 return 4;
212}
213
214bool HistoryModel::hasChildren(const QModelIndex &parent) const
215{
216 if (!parent.isValid()) {
217 return true;
218 }
219
221
222 return item ? item->isTopLevel() : false;
223}
224
225HistoryItem* HistoryModel::itemFromIndex(const QModelIndex &index) const
226{
227 if (index.isValid()) {
228 auto* item = static_cast<HistoryItem*>(index.internalPointer());
229
230 if (item) {
231 return item;
232 }
233 }
234
235 return m_rootItem;
236}
237
238void HistoryModel::removeTopLevelIndexes(const QList<QPersistentModelIndex> &indexes)
239{
240 for (const QPersistentModelIndex &index : indexes) {
241 if (index.parent().isValid()) {
242 continue;
243 }
244
245 int row = index.row();
246 HistoryItem* item = m_rootItem->child(row);
247
248 if (!item) {
249 return;
250 }
251
252 beginRemoveRows(QModelIndex(), row, row);
253 delete item;
254 endRemoveRows();
255
256 if (item == m_todayItem) {
257 m_todayItem = nullptr;
258 }
259 }
260}
261
262void HistoryModel::resetHistory()
263{
264 beginResetModel();
265
266 delete m_rootItem;
267 m_todayItem = nullptr;
268 m_rootItem = new HistoryItem(nullptr);
269
270 init();
271
272 endResetModel();
273}
274
275bool HistoryModel::canFetchMore(const QModelIndex &parent) const
276{
277 HistoryItem* parentItem = itemFromIndex(parent);
278
279 return parentItem ? parentItem->canFetchMore : false;
280}
281
282void HistoryModel::fetchMore(const QModelIndex &parent)
283{
284 HistoryItem* parentItem = itemFromIndex(parent);
285
286 if (!parent.isValid() || !parentItem) {
287 return;
288 }
289
290 parentItem->canFetchMore = false;
291
292 QList<int> idList;
293 for (int i = 0; i < parentItem->childCount(); ++i) {
294 idList.append(parentItem->child(i)->historyEntry.id);
295 }
296
297 QSqlQuery query(SqlDatabase::instance()->database());
298 query.prepare(QSL("SELECT id, count, title, url, date FROM history WHERE date BETWEEN ? AND ? ORDER BY date DESC"));
299 query.addBindValue(parentItem->endTimestamp());
300 query.addBindValue(parentItem->startTimestamp());
301 query.exec();
302
303 QVector<HistoryEntry> list;
304
305 while (query.next()) {
306 HistoryEntry entry;
307 entry.id = query.value(0).toInt();
308 entry.count = query.value(1).toInt();
309 entry.title = query.value(2).toString();
310 entry.url = query.value(3).toUrl();
311 entry.date = QDateTime::fromMSecsSinceEpoch(query.value(4).toLongLong());
312 entry.urlString = QString::fromUtf8(entry.url.toEncoded());
313
314 if (!idList.contains(entry.id)) {
315 list.append(entry);
316 }
317 }
318
319 if (list.isEmpty()) {
320 return;
321 }
322
323 beginInsertRows(parent, 0, list.size() - 1);
324
325 for (const HistoryEntry &entry : std::as_const(list)) {
326 auto* newItem = new HistoryItem(parentItem);
327 newItem->historyEntry = entry;
328 }
329
330 endInsertRows();
331}
332
333void HistoryModel::historyEntryAdded(const HistoryEntry &entry)
334{
335 if (!m_todayItem) {
336 beginInsertRows(QModelIndex(), 0, 0);
337
338 m_todayItem = new HistoryItem(nullptr);
339 m_todayItem->setStartTimestamp(-1);
340 m_todayItem->setEndTimestamp(QDateTime(QDate::currentDate(), QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch());
341 m_todayItem->title = tr("Today");
342
343 m_rootItem->prependChild(m_todayItem);
344
345 endInsertRows();
346 }
347
348 beginInsertRows(createIndex(0, 0, m_todayItem), 0, 0);
349
350 auto* item = new HistoryItem();
351 item->historyEntry = entry;
352
353 m_todayItem->prependChild(item);
354
355 endInsertRows();
356}
357
358void HistoryModel::historyEntryDeleted(const HistoryEntry &entry)
359{
360 HistoryItem* item = findHistoryItem(entry);
361 if (!item) {
362 return;
363 }
364
365 HistoryItem* parentItem = item->parent();
366 int row = item->row();
367
368 beginRemoveRows(createIndex(parentItem->row(), 0, parentItem), row, row);
369 delete item;
370 endRemoveRows();
371
372 checkEmptyParentItem(parentItem);
373}
374
375void HistoryModel::historyEntryEdited(const HistoryEntry &before, const HistoryEntry &after)
376{
377#if 0
378 HistoryItem* item = findHistoryItem(before);
379
380 if (item) {
381 HistoryItem* parentItem = item->parent();
382 const QModelIndex sourceParent = createIndex(parentItem->row(), 0, parentItem);
383 const QModelIndex destinationParent = createIndex(m_todayItem->row(), 0, m_todayItem);
384 int row = item->row();
385
386 beginMoveRows(sourceParent, row, row, destinationParent, 0);
387 item->historyEntry = after;
388 item->refreshIcon();
389 item->changeParent(m_todayItem);
390 endMoveRows(); // This line sometimes throw "std::bad_alloc" ... I don't know why ?!
391
392 checkEmptyParentItem(parentItem);
393 }
394 else {
395 historyEntryAdded(after);
396 }
397#endif
398 historyEntryDeleted(before);
399 historyEntryAdded(after);
400}
401
402HistoryItem* HistoryModel::findHistoryItem(const HistoryEntry &entry)
403{
404 HistoryItem* parentItem = nullptr;
405 qint64 timestamp = entry.date.toMSecsSinceEpoch();
406
407 for (int i = 0; i < m_rootItem->childCount(); ++i) {
408 HistoryItem* item = m_rootItem->child(i);
409
410 if (item->endTimestamp() < timestamp) {
411 parentItem = item;
412 break;
413 }
414 }
415
416 if (!parentItem) {
417 return nullptr;
418 }
419
420 for (int i = 0; i < parentItem->childCount(); ++i) {
421 HistoryItem* item = parentItem->child(i);
422 if (item->historyEntry.id == entry.id) {
423 return item;
424 }
425 }
426
427 return nullptr;
428}
429
430void HistoryModel::checkEmptyParentItem(HistoryItem* item)
431{
432 if (item->childCount() == 0 && item->isTopLevel()) {
433 int row = item->row();
434
435 beginRemoveRows(QModelIndex(), row, row);
436 delete item;
437 endRemoveRows();
438
439 if (item == m_todayItem) {
440 m_todayItem = nullptr;
441 }
442 }
443}
444
445void HistoryModel::init()
446{
447 QSqlQuery query(SqlDatabase::instance()->database());
448 query.exec(QSL("SELECT MIN(date) FROM history"));
449 if (!query.next()) {
450 return;
451 }
452
453 const qint64 minTimestamp = query.value(0).toLongLong();
454 if (minTimestamp <= 0) {
455 return;
456 }
457
458 const QDate today = QDate::currentDate();
459 const QDate week = today.addDays(1 - today.dayOfWeek());
460 const QDate month = QDate(today.year(), today.month(), 1);
461 const qint64 currentTimestamp = QDateTime::currentMSecsSinceEpoch();
462
463 qint64 timestamp = currentTimestamp;
464 while (timestamp > minTimestamp) {
465 QDate timestampDate = QDateTime::fromMSecsSinceEpoch(timestamp).date();
466 qint64 endTimestamp;
467 QString itemName;
468
469 if (timestampDate == today) {
470 endTimestamp = QDateTime(today, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
471
472 itemName = tr("Today");
473 }
474 else if (timestampDate >= week) {
475 endTimestamp = QDateTime(week, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
476
477 itemName = tr("This Week");
478 }
479 else if (timestampDate.month() == month.month() && timestampDate.year() == month.year()) {
480 endTimestamp = QDateTime(month, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
481
482 itemName = tr("This Month");
483 }
484 else {
485 QDate startDate(timestampDate.year(), timestampDate.month(), timestampDate.daysInMonth());
486 QDate endDate(startDate.year(), startDate.month(), 1);
487
488 timestamp = QDateTime(startDate, QTime(23, 59, 59), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
489 endTimestamp = QDateTime(endDate, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
490 itemName = QSL("%1 %2").arg(History::titleCaseLocalizedMonth(timestampDate.month()), QString::number(timestampDate.year()));
491 }
492
493 QSqlQuery query(SqlDatabase::instance()->database());
494 query.prepare(QSL("SELECT id FROM history WHERE date BETWEEN ? AND ? LIMIT 1"));
495 query.addBindValue(endTimestamp);
496 query.addBindValue(timestamp);
497 query.exec();
498
499 if (query.next()) {
500 auto* item = new HistoryItem(m_rootItem);
501 item->setStartTimestamp(timestamp == currentTimestamp ? -1 : timestamp);
502 item->setEndTimestamp(endTimestamp);
503 item->title = itemName;
504 item->canFetchMore = true;
505
506 if (timestamp == currentTimestamp) {
507 m_todayItem = item;
508 }
509 }
510
511 timestamp = endTimestamp - 1;
512 }
513}
514
515// HistoryFilterModel
516HistoryFilterModel::HistoryFilterModel(QAbstractItemModel* parent)
517 : QSortFilterProxyModel(parent)
518{
519 setSourceModel(parent);
520 setFilterCaseSensitivity(Qt::CaseInsensitive);
521
522 m_filterTimer = new QTimer(this);
523 m_filterTimer->setSingleShot(true);
524 m_filterTimer->setInterval(300);
525
526 connect(m_filterTimer, &QTimer::timeout, this, &HistoryFilterModel::startFiltering);
527}
528
530{
531 m_pattern = pattern;
532
533 m_filterTimer->start();
534}
535
536void HistoryFilterModel::startFiltering()
537{
538 if (m_pattern.isEmpty()) {
539 Q_EMIT collapseAllItems();
540 QSortFilterProxyModel::setFilterFixedString(m_pattern);
541 return;
542 }
543
544 QApplication::setOverrideCursor(Qt::WaitCursor);
545
546 // Expand all items also calls fetchmore
547 Q_EMIT expandAllItems();
548
549 QSortFilterProxyModel::setFilterFixedString(m_pattern);
550
551 QApplication::restoreOverrideCursor();
552}
553
554bool HistoryFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
555{
556 const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
557
558 if (index.data(HistoryModel::IsTopLevelRole).toBool()) {
559 return true;
560 }
561
562 return (index.data(HistoryModel::UrlStringRole).toString().contains(m_pattern, Qt::CaseInsensitive) ||
563 index.data(HistoryModel::TitleRole).toString().contains(m_pattern, Qt::CaseInsensitive));
564}
565
567{
568 return m_pattern.isEmpty();
569}
HistoryFilterModel(QAbstractItemModel *parent)
bool isPatternEmpty() const
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
void setFilterFixedString(const QString &pattern)
void historyEntryDeleted(const HistoryEntry &entry)
void historyEntryEdited(const HistoryEntry &before, const HistoryEntry &after)
void resetHistory()
static QString titleCaseLocalizedMonth(int month)
Definition: history.cpp:269
void historyEntryAdded(const HistoryEntry &entry)
void setIcon(const QIcon &icon)
HistoryItem * parent() const
Definition: historyitem.cpp:45
bool canFetchMore
Definition: historyitem.h:61
qint64 startTimestamp() const
qint64 endTimestamp() const
void setStartTimestamp(qint64 start)
HistoryItem * child(int row) const
Definition: historyitem.cpp:94
QString title
Definition: historyitem.h:60
QIcon icon() const
void changeParent(HistoryItem *parent)
Definition: historyitem.cpp:32
void setEndTimestamp(qint64 end)
HistoryEntry historyEntry
Definition: historyitem.h:59
void prependChild(HistoryItem *child)
Definition: historyitem.cpp:50
bool isTopLevel() const
int childCount() const
QModelIndex parent(const QModelIndex &child) const override
Qt::ItemFlags flags(const QModelIndex &index) const override
QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const override
int columnCount(const QModelIndex &parent=QModelIndex()) const override
bool setData(const QModelIndex &index, const QVariant &value, int role) override
int rowCount(const QModelIndex &parent=QModelIndex()) const override
HistoryModel(History *history)
QVariant data(const QModelIndex &index, int role) const override
void fetchMore(const QModelIndex &parent) override
bool canFetchMore(const QModelIndex &parent) const override
void removeTopLevelIndexes(const QList< QPersistentModelIndex > &indexes)
bool hasChildren(const QModelIndex &parent) const override
HistoryItem * itemFromIndex(const QModelIndex &index) const
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
static QIcon emptyWebIcon()
static SqlDatabase * instance()
int value(const QColor &c)
Definition: colors.cpp:238
i
Definition: i18n.py:23
#define QSL(x)
Definition: qzcommon.h:40
Definition: history.h:39
QDateTime date
Definition: history.h:42
QString title
Definition: history.h:45
int id
Definition: history.h:40
QUrl url
Definition: history.h:43
QString urlString
Definition: history.h:44
int count
Definition: history.h:41