File indexing completed on 2024-05-12 04:58:06

0001 /* ============================================================
0002 * Falkon - Qt web browser
0003 * Copyright (C) 2010-2017 David Rosca <nowrep@gmail.com>
0004 *
0005 * This program is free software: you can redistribute it and/or modify
0006 * it under the terms of the GNU General Public License as published by
0007 * the Free Software Foundation, either version 3 of the License, or
0008 * (at your option) any later version.
0009 *
0010 * This program is distributed in the hope that it will be useful,
0011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 * GNU General Public License for more details.
0014 *
0015 * You should have received a copy of the GNU General Public License
0016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0017 * ============================================================ */
0018 #include "historymodel.h"
0019 #include "historyitem.h"
0020 #include "iconprovider.h"
0021 #include "sqldatabase.h"
0022 
0023 #include <QApplication>
0024 #include <QDateTime>
0025 #include <QTimeZone>
0026 #include <QTimer>
0027 
0028 static QString dateTimeToString(const QDateTime &dateTime)
0029 {
0030     const QDateTime current = QDateTime::currentDateTime();
0031     if (current.date() == dateTime.date()) {
0032         return dateTime.time().toString(QSL("h:mm"));
0033     }
0034 
0035     return dateTime.toString(QSL("d.M.yyyy h:mm"));
0036 }
0037 
0038 HistoryModel::HistoryModel(History* history)
0039     : QAbstractItemModel(history)
0040     , m_rootItem(new HistoryItem(nullptr))
0041     , m_todayItem(nullptr)
0042     , m_history(history)
0043 {
0044     init();
0045 
0046     connect(m_history, &History::resetHistory, this, &HistoryModel::resetHistory);
0047     connect(m_history, &History::historyEntryAdded, this, &HistoryModel::historyEntryAdded);
0048     connect(m_history, &History::historyEntryDeleted, this, &HistoryModel::historyEntryDeleted);
0049     connect(m_history, &History::historyEntryEdited, this, &HistoryModel::historyEntryEdited);
0050 }
0051 
0052 QVariant HistoryModel::headerData(int section, Qt::Orientation orientation, int role) const
0053 {
0054     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0055         switch (section) {
0056         case 0:
0057             return tr("Title");
0058         case 1:
0059             return tr("Address");
0060         case 2:
0061             return tr("Visit Date");
0062         case 3:
0063             return tr("Visit Count");
0064         }
0065     }
0066 
0067     return QAbstractItemModel::headerData(section, orientation, role);
0068 }
0069 
0070 QVariant HistoryModel::data(const QModelIndex &index, int role) const
0071 {
0072     HistoryItem* item = itemFromIndex(index);
0073 
0074     if (index.row() < 0 || !item) {
0075         return {};
0076     }
0077 
0078     if (item->isTopLevel()) {
0079         switch (role) {
0080         case IsTopLevelRole:
0081             return true;
0082         case TimestampStartRole:
0083             return item->startTimestamp();
0084         case TimestampEndRole:
0085             return item->endTimestamp();
0086         case Qt::DisplayRole:
0087         case Qt::EditRole:
0088             return index.column() == 0 ? item->title : QVariant();
0089         case Qt::DecorationRole:
0090             return index.column() == 0 ? QIcon::fromTheme(QSL("view-calendar"), QIcon(QSL(":/icons/menu/history_entry.svg"))) : QVariant();
0091         }
0092 
0093         return {};
0094     }
0095 
0096     const HistoryEntry entry = item->historyEntry;
0097 
0098     switch (role) {
0099     case IdRole:
0100         return entry.id;
0101     case TitleRole:
0102         return entry.title;
0103     case UrlRole:
0104         return entry.url;
0105     case UrlStringRole:
0106         return entry.urlString;
0107     case IconRole:
0108         return item->icon();
0109     case IsTopLevelRole:
0110         return false;
0111     case TimestampStartRole:
0112         return -1;
0113     case TimestampEndRole:
0114         return -1;
0115     case Qt::ToolTipRole:
0116         if (index.column() == 0) {
0117             return QSL("%1\n%2").arg(entry.title, entry.urlString);
0118         }
0119         // fallthrough
0120     case Qt::DisplayRole:
0121     case Qt::EditRole:
0122         switch (index.column()) {
0123         case 0:
0124             return entry.title;
0125         case 1:
0126             return entry.urlString;
0127         case 2:
0128             return dateTimeToString(entry.date);
0129         case 3:
0130             return entry.count;
0131         }
0132         break;
0133     case Qt::DecorationRole:
0134         if (index.column() == 0) {
0135             return item->icon().isNull() ? IconProvider::emptyWebIcon() : item->icon();
0136         }
0137     }
0138 
0139     return {};
0140 }
0141 
0142 bool HistoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
0143 {
0144     HistoryItem* item = itemFromIndex(index);
0145 
0146     if (index.row() < 0 || !item || item->isTopLevel()) {
0147         return false;
0148     }
0149 
0150     if (role == IconRole) {
0151         item->setIcon(value.value<QIcon>());
0152         Q_EMIT dataChanged(index, index);
0153         return true;
0154     }
0155 
0156     return false;
0157 }
0158 
0159 QModelIndex HistoryModel::index(int row, int column, const QModelIndex &parent) const
0160 {
0161     if (!hasIndex(row, column, parent)) {
0162         return {};
0163     }
0164 
0165     HistoryItem* parentItem = itemFromIndex(parent);
0166     HistoryItem* childItem = parentItem->child(row);
0167 
0168     return childItem ? createIndex(row, column, childItem) : QModelIndex();
0169 }
0170 
0171 QModelIndex HistoryModel::parent(const QModelIndex &index) const
0172 {
0173     if (!index.isValid()) {
0174         return {};
0175     }
0176 
0177     HistoryItem* childItem = itemFromIndex(index);
0178     HistoryItem* parentItem = childItem->parent();
0179 
0180     if (!parentItem || parentItem == m_rootItem) {
0181         return {};
0182     }
0183 
0184     return createIndex(parentItem->row(), 0, parentItem);
0185 }
0186 
0187 Qt::ItemFlags HistoryModel::flags(const QModelIndex &index) const
0188 {
0189     if (!index.isValid()) {
0190         return Qt::NoItemFlags;
0191     }
0192 
0193     return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
0194 }
0195 
0196 int HistoryModel::rowCount(const QModelIndex &parent) const
0197 {
0198     if (parent.column() > 0) {
0199         return 0;
0200     }
0201 
0202     HistoryItem* parentItem = itemFromIndex(parent);
0203 
0204     return parentItem->childCount();
0205 }
0206 
0207 int HistoryModel::columnCount(const QModelIndex &parent) const
0208 {
0209     Q_UNUSED(parent)
0210 
0211     return 4;
0212 }
0213 
0214 bool HistoryModel::hasChildren(const QModelIndex &parent) const
0215 {
0216     if (!parent.isValid()) {
0217         return true;
0218     }
0219 
0220     HistoryItem* item = itemFromIndex(parent);
0221 
0222     return item ? item->isTopLevel() : false;
0223 }
0224 
0225 HistoryItem* HistoryModel::itemFromIndex(const QModelIndex &index) const
0226 {
0227     if (index.isValid()) {
0228         auto* item = static_cast<HistoryItem*>(index.internalPointer());
0229 
0230         if (item) {
0231             return item;
0232         }
0233     }
0234 
0235     return m_rootItem;
0236 }
0237 
0238 void HistoryModel::removeTopLevelIndexes(const QList<QPersistentModelIndex> &indexes)
0239 {
0240     for (const QPersistentModelIndex &index : indexes) {
0241         if (index.parent().isValid()) {
0242             continue;
0243         }
0244 
0245         int row = index.row();
0246         HistoryItem* item = m_rootItem->child(row);
0247 
0248         if (!item) {
0249             return;
0250         }
0251 
0252         beginRemoveRows(QModelIndex(), row, row);
0253         delete item;
0254         endRemoveRows();
0255 
0256         if (item == m_todayItem) {
0257             m_todayItem = nullptr;
0258         }
0259     }
0260 }
0261 
0262 void HistoryModel::resetHistory()
0263 {
0264     beginResetModel();
0265 
0266     delete m_rootItem;
0267     m_todayItem = nullptr;
0268     m_rootItem = new HistoryItem(nullptr);
0269 
0270     init();
0271 
0272     endResetModel();
0273 }
0274 
0275 bool HistoryModel::canFetchMore(const QModelIndex &parent) const
0276 {
0277     HistoryItem* parentItem = itemFromIndex(parent);
0278 
0279     return parentItem ? parentItem->canFetchMore : false;
0280 }
0281 
0282 void HistoryModel::fetchMore(const QModelIndex &parent)
0283 {
0284     HistoryItem* parentItem = itemFromIndex(parent);
0285 
0286     if (!parent.isValid() || !parentItem) {
0287         return;
0288     }
0289 
0290     parentItem->canFetchMore = false;
0291 
0292     QList<int> idList;
0293     for (int i = 0; i < parentItem->childCount(); ++i) {
0294         idList.append(parentItem->child(i)->historyEntry.id);
0295     }
0296 
0297     QSqlQuery query(SqlDatabase::instance()->database());
0298     query.prepare(QSL("SELECT id, count, title, url, date FROM history WHERE date BETWEEN ? AND ? ORDER BY date DESC"));
0299     query.addBindValue(parentItem->endTimestamp());
0300     query.addBindValue(parentItem->startTimestamp());
0301     query.exec();
0302 
0303     QVector<HistoryEntry> list;
0304 
0305     while (query.next()) {
0306         HistoryEntry entry;
0307         entry.id = query.value(0).toInt();
0308         entry.count = query.value(1).toInt();
0309         entry.title = query.value(2).toString();
0310         entry.url = query.value(3).toUrl();
0311         entry.date = QDateTime::fromMSecsSinceEpoch(query.value(4).toLongLong());
0312         entry.urlString = QString::fromUtf8(entry.url.toEncoded());
0313 
0314         if (!idList.contains(entry.id)) {
0315             list.append(entry);
0316         }
0317     }
0318 
0319     if (list.isEmpty()) {
0320         return;
0321     }
0322 
0323     beginInsertRows(parent, 0, list.size() - 1);
0324 
0325     for (const HistoryEntry &entry : std::as_const(list)) {
0326         auto* newItem = new HistoryItem(parentItem);
0327         newItem->historyEntry = entry;
0328     }
0329 
0330     endInsertRows();
0331 }
0332 
0333 void HistoryModel::historyEntryAdded(const HistoryEntry &entry)
0334 {
0335     if (!m_todayItem) {
0336         beginInsertRows(QModelIndex(), 0, 0);
0337 
0338         m_todayItem = new HistoryItem(nullptr);
0339         m_todayItem->setStartTimestamp(-1);
0340         m_todayItem->setEndTimestamp(QDateTime(QDate::currentDate(), QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch());
0341         m_todayItem->title = tr("Today");
0342 
0343         m_rootItem->prependChild(m_todayItem);
0344 
0345         endInsertRows();
0346     }
0347 
0348     beginInsertRows(createIndex(0, 0, m_todayItem), 0, 0);
0349 
0350     auto* item = new HistoryItem();
0351     item->historyEntry = entry;
0352 
0353     m_todayItem->prependChild(item);
0354 
0355     endInsertRows();
0356 }
0357 
0358 void HistoryModel::historyEntryDeleted(const HistoryEntry &entry)
0359 {
0360     HistoryItem* item = findHistoryItem(entry);
0361     if (!item) {
0362         return;
0363     }
0364 
0365     HistoryItem* parentItem = item->parent();
0366     int row = item->row();
0367 
0368     beginRemoveRows(createIndex(parentItem->row(), 0, parentItem), row, row);
0369     delete item;
0370     endRemoveRows();
0371 
0372     checkEmptyParentItem(parentItem);
0373 }
0374 
0375 void HistoryModel::historyEntryEdited(const HistoryEntry &before, const HistoryEntry &after)
0376 {
0377 #if 0
0378     HistoryItem* item = findHistoryItem(before);
0379 
0380     if (item) {
0381         HistoryItem* parentItem = item->parent();
0382         const QModelIndex sourceParent = createIndex(parentItem->row(), 0, parentItem);
0383         const QModelIndex destinationParent = createIndex(m_todayItem->row(), 0, m_todayItem);
0384         int row = item->row();
0385 
0386         beginMoveRows(sourceParent, row, row, destinationParent, 0);
0387         item->historyEntry = after;
0388         item->refreshIcon();
0389         item->changeParent(m_todayItem);
0390         endMoveRows(); // This line sometimes throw "std::bad_alloc" ... I don't know why ?!
0391 
0392         checkEmptyParentItem(parentItem);
0393     }
0394     else {
0395         historyEntryAdded(after);
0396     }
0397 #endif
0398     historyEntryDeleted(before);
0399     historyEntryAdded(after);
0400 }
0401 
0402 HistoryItem* HistoryModel::findHistoryItem(const HistoryEntry &entry)
0403 {
0404     HistoryItem* parentItem = nullptr;
0405     qint64 timestamp = entry.date.toMSecsSinceEpoch();
0406 
0407     for (int i = 0; i < m_rootItem->childCount(); ++i) {
0408         HistoryItem* item = m_rootItem->child(i);
0409 
0410         if (item->endTimestamp() < timestamp) {
0411             parentItem = item;
0412             break;
0413         }
0414     }
0415 
0416     if (!parentItem) {
0417         return nullptr;
0418     }
0419 
0420     for (int i = 0; i < parentItem->childCount(); ++i) {
0421         HistoryItem* item = parentItem->child(i);
0422         if (item->historyEntry.id == entry.id) {
0423             return item;
0424         }
0425     }
0426 
0427     return nullptr;
0428 }
0429 
0430 void HistoryModel::checkEmptyParentItem(HistoryItem* item)
0431 {
0432     if (item->childCount() == 0 && item->isTopLevel()) {
0433         int row = item->row();
0434 
0435         beginRemoveRows(QModelIndex(), row, row);
0436         delete item;
0437         endRemoveRows();
0438 
0439         if (item == m_todayItem) {
0440             m_todayItem = nullptr;
0441         }
0442     }
0443 }
0444 
0445 void HistoryModel::init()
0446 {
0447     QSqlQuery query(SqlDatabase::instance()->database());
0448     query.exec(QSL("SELECT MIN(date) FROM history"));
0449     if (!query.next()) {
0450         return;
0451     }
0452 
0453     const qint64 minTimestamp = query.value(0).toLongLong();
0454     if (minTimestamp <= 0) {
0455         return;
0456     }
0457 
0458     const QDate today = QDate::currentDate();
0459     const QDate week = today.addDays(1 - today.dayOfWeek());
0460     const QDate month = QDate(today.year(), today.month(), 1);
0461     const qint64 currentTimestamp = QDateTime::currentMSecsSinceEpoch();
0462 
0463     qint64 timestamp = currentTimestamp;
0464     while (timestamp > minTimestamp) {
0465         QDate timestampDate = QDateTime::fromMSecsSinceEpoch(timestamp).date();
0466         qint64 endTimestamp;
0467         QString itemName;
0468 
0469         if (timestampDate == today) {
0470             endTimestamp = QDateTime(today, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
0471 
0472             itemName = tr("Today");
0473         }
0474         else if (timestampDate >= week) {
0475             endTimestamp = QDateTime(week, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
0476 
0477             itemName = tr("This Week");
0478         }
0479         else if (timestampDate.month() == month.month() && timestampDate.year() == month.year()) {
0480             endTimestamp = QDateTime(month, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
0481 
0482             itemName = tr("This Month");
0483         }
0484         else {
0485             QDate startDate(timestampDate.year(), timestampDate.month(), timestampDate.daysInMonth());
0486             QDate endDate(startDate.year(), startDate.month(), 1);
0487 
0488             timestamp = QDateTime(startDate, QTime(23, 59, 59), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
0489             endTimestamp = QDateTime(endDate, QTime(), QTimeZone::systemTimeZone()).toMSecsSinceEpoch();
0490             itemName = QSL("%1 %2").arg(History::titleCaseLocalizedMonth(timestampDate.month()), QString::number(timestampDate.year()));
0491         }
0492 
0493         QSqlQuery query(SqlDatabase::instance()->database());
0494         query.prepare(QSL("SELECT id FROM history WHERE date BETWEEN ? AND ? LIMIT 1"));
0495         query.addBindValue(endTimestamp);
0496         query.addBindValue(timestamp);
0497         query.exec();
0498 
0499         if (query.next()) {
0500             auto* item = new HistoryItem(m_rootItem);
0501             item->setStartTimestamp(timestamp == currentTimestamp ? -1 : timestamp);
0502             item->setEndTimestamp(endTimestamp);
0503             item->title = itemName;
0504             item->canFetchMore = true;
0505 
0506             if (timestamp == currentTimestamp) {
0507                 m_todayItem = item;
0508             }
0509         }
0510 
0511         timestamp = endTimestamp - 1;
0512     }
0513 }
0514 
0515 // HistoryFilterModel
0516 HistoryFilterModel::HistoryFilterModel(QAbstractItemModel* parent)
0517     : QSortFilterProxyModel(parent)
0518 {
0519     setSourceModel(parent);
0520     setFilterCaseSensitivity(Qt::CaseInsensitive);
0521 
0522     m_filterTimer = new QTimer(this);
0523     m_filterTimer->setSingleShot(true);
0524     m_filterTimer->setInterval(300);
0525 
0526     connect(m_filterTimer, &QTimer::timeout, this, &HistoryFilterModel::startFiltering);
0527 }
0528 
0529 void HistoryFilterModel::setFilterFixedString(const QString &pattern)
0530 {
0531     m_pattern = pattern;
0532 
0533     m_filterTimer->start();
0534 }
0535 
0536 void HistoryFilterModel::startFiltering()
0537 {
0538     if (m_pattern.isEmpty()) {
0539         Q_EMIT collapseAllItems();
0540         QSortFilterProxyModel::setFilterFixedString(m_pattern);
0541         return;
0542     }
0543 
0544     QApplication::setOverrideCursor(Qt::WaitCursor);
0545 
0546     // Expand all items also calls fetchmore
0547     Q_EMIT expandAllItems();
0548 
0549     QSortFilterProxyModel::setFilterFixedString(m_pattern);
0550 
0551     QApplication::restoreOverrideCursor();
0552 }
0553 
0554 bool HistoryFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
0555 {
0556     const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
0557 
0558     if (index.data(HistoryModel::IsTopLevelRole).toBool()) {
0559         return true;
0560     }
0561 
0562     return (index.data(HistoryModel::UrlStringRole).toString().contains(m_pattern, Qt::CaseInsensitive) ||
0563             index.data(HistoryModel::TitleRole).toString().contains(m_pattern, Qt::CaseInsensitive));
0564 }
0565 
0566 bool HistoryFilterModel::isPatternEmpty() const
0567 {
0568     return m_pattern.isEmpty();
0569 }