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 }