history.cpp Example File

webenginewidgets/demobrowser/history.cpp
/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/
#include "history.h" #include "autosaver.h" #include "browserapplication.h" #include <QtCore/QBuffer> #include <QtCore/QDir> #include <QtCore/QFile> #include <QtCore/QFileInfo> #include <QtCore/QSettings> #include <QtCore/QTemporaryFile> #include <QtCore/QTextStream> #include <QtCore/QtAlgorithms> #include <QtGui/QClipboard> #include <QtGui/QDesktopServices> #include <QtWidgets/QHeaderView> #include <QtWidgets/QStyle> #include <QWebEngineSettings> #include <QtCore/QDebug> static const unsigned int HISTORY_VERSION = 23; HistoryManager::HistoryManager(QObject *parent) : QObject(parent) , m_saveTimer(new AutoSaver(this)) , m_historyLimit(30) , m_historyModel(0) , m_historyFilterModel(0) , m_historyTreeModel(0) { m_expiredTimer.setSingleShot(true); connect(&m_expiredTimer, SIGNAL(timeout()), this, SLOT(checkForExpired())); connect(this, SIGNAL(entryAdded(HistoryItem)), m_saveTimer, SLOT(changeOccurred())); connect(this, SIGNAL(entryRemoved(HistoryItem)), m_saveTimer, SLOT(changeOccurred())); load(); m_historyModel = new HistoryModel(this, this); m_historyFilterModel = new HistoryFilterModel(m_historyModel, this); m_historyTreeModel = new HistoryTreeModel(m_historyFilterModel, this); } HistoryManager::~HistoryManager() { m_saveTimer->saveIfNeccessary(); } QList<HistoryItem> &HistoryManager::history() { return m_history; } bool HistoryManager::historyContains(const QString &url) const { return m_historyFilterModel->historyContains(url); } void HistoryManager::addHistoryEntry(const QString &url) { QUrl cleanUrl(url); cleanUrl.setPassword(QString()); cleanUrl.setHost(cleanUrl.host().toLower()); HistoryItem item(cleanUrl.toString(), QDateTime::currentDateTime()); addHistoryItem(item); } void HistoryManager::removeHistoryEntry(const QString &url) { QUrl cleanUrl(url); cleanUrl.setPassword(QString()); cleanUrl.setHost(cleanUrl.host().toLower()); HistoryItem item(cleanUrl.toString(), QDateTime::currentDateTime()); removeHistoryItem(item); } void HistoryManager::setHistory(const QList<HistoryItem> &history, bool loadedAndSorted) { m_history = history; // verify that it is sorted by date if (!loadedAndSorted) qSort(m_history.begin(), m_history.end()); checkForExpired(loadedAndSorted); if (loadedAndSorted) { m_lastSavedUrl = m_history.value(0).url; } else { m_lastSavedUrl = QString(); m_saveTimer->changeOccurred(); } emit historyReset(); } HistoryModel *HistoryManager::historyModel() const { return m_historyModel; } HistoryFilterModel *HistoryManager::historyFilterModel() const { return m_historyFilterModel; } HistoryTreeModel *HistoryManager::historyTreeModel() const { return m_historyTreeModel; } void HistoryManager::checkForExpired(bool removeEntriesDirectly) { if (m_historyLimit < 0 || m_history.isEmpty()) return; QDateTime now = QDateTime::currentDateTime(); int nextTimeout = 0; while (!m_history.isEmpty()) { QDateTime checkForExpired = m_history.last().dateTime; checkForExpired.setDate(checkForExpired.date().addDays(m_historyLimit)); if (now.daysTo(checkForExpired) > 7) { // check at most in a week to prevent int overflows on the timer nextTimeout = 7 * 86400; } else { nextTimeout = now.secsTo(checkForExpired); } if (nextTimeout > 0) break; const HistoryItem& item = m_history.last(); // remove from saved file also m_lastSavedUrl = QString(); if (removeEntriesDirectly) m_history.takeLast(); else emit entryRemoved(item); } if (nextTimeout > 0) m_expiredTimer.start(nextTimeout * 1000); } void HistoryManager::addHistoryItem(const HistoryItem &item) { if (BrowserApplication::instance()->privateBrowsing()) return; emit entryAdded(item); if (m_history.count() == 1) checkForExpired(); } void HistoryManager::removeHistoryItem(const HistoryItem &item) { for (int i = m_history.count() - 1 ; i >= 0; --i) { if (item.url == m_history.at(i).url) { //delete all related entries with that url emit entryRemoved(m_history.at(i)); } } } void HistoryManager::updateHistoryItem(const QUrl &url, const QString &title) { for (int i = 0; i < m_history.count(); ++i) { if (url == m_history.at(i).url) { m_history[i].title = title; m_saveTimer->changeOccurred(); if (m_lastSavedUrl.isEmpty()) m_lastSavedUrl = m_history.at(i).url; emit entryUpdated(i); break; } } } int HistoryManager::historyLimit() const { return m_historyLimit; } void HistoryManager::setHistoryLimit(int limit) { if (m_historyLimit == limit) return; m_historyLimit = limit; checkForExpired(); m_saveTimer->changeOccurred(); } void HistoryManager::clear() { m_lastSavedUrl = QString(); emit historyReset(); m_saveTimer->changeOccurred(); m_saveTimer->saveIfNeccessary(); } void HistoryManager::loadSettings() { // load settings QSettings settings; settings.beginGroup(QLatin1String("history")); m_historyLimit = settings.value(QLatin1String("historyLimit"), 30).toInt(); } void HistoryManager::load() { loadSettings(); QFile historyFile(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/history")); if (!historyFile.exists()) return; if (!historyFile.open(QFile::ReadOnly)) { qWarning() << "Unable to open history file" << historyFile.fileName(); return; } QList<HistoryItem> list; QDataStream in(&historyFile); // Double check that the history file is sorted as it is read in bool needToSort = false; HistoryItem lastInsertedItem; QByteArray data; QDataStream stream; QBuffer buffer; stream.setDevice(&buffer); while (!historyFile.atEnd()) { in >> data; buffer.close(); buffer.setBuffer(&data); buffer.open(QIODevice::ReadOnly); quint32 ver; stream >> ver; if (ver != HISTORY_VERSION) continue; HistoryItem item; stream >> item.url; stream >> item.dateTime; stream >> item.title; if (!item.dateTime.isValid()) continue; if (item == lastInsertedItem) { if (lastInsertedItem.title.isEmpty() && !list.isEmpty()) list[0].title = item.title; continue; } if (!needToSort && !list.isEmpty() && lastInsertedItem < item) needToSort = true; list.prepend(item); lastInsertedItem = item; } if (needToSort) qSort(list.begin(), list.end()); setHistory(list, true); // If we had to sort re-write the whole history sorted if (needToSort) { m_lastSavedUrl = QString(); m_saveTimer->changeOccurred(); } } void HistoryManager::save() { QSettings settings; settings.beginGroup(QLatin1String("history")); settings.setValue(QLatin1String("historyLimit"), m_historyLimit); bool saveAll = m_lastSavedUrl.isEmpty(); int first = m_history.count() - 1; if (!saveAll) { // find the first one to save for (int i = 0; i < m_history.count(); ++i) { if (m_history.at(i).url == m_lastSavedUrl) { first = i - 1; break; } } } if (first == m_history.count() - 1) saveAll = true; QString directory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); if (directory.isEmpty()) directory = QDir::homePath() + QLatin1String("/.") + QCoreApplication::applicationName(); if (!QFile::exists(directory)) { QDir dir; dir.mkpath(directory); } QFile historyFile(directory + QLatin1String("/history")); // When saving everything use a temporary file to prevent possible data loss. QTemporaryFile tempFile; tempFile.setAutoRemove(false); bool open = false; if (saveAll) { open = tempFile.open(); } else { open = historyFile.open(QFile::Append); } if (!open) { qWarning() << "Unable to open history file for saving" << (saveAll ? tempFile.fileName() : historyFile.fileName()); return; } QDataStream out(saveAll ? &tempFile : &historyFile); for (int i = first; i >= 0; --i) { QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); HistoryItem item = m_history.at(i); stream << HISTORY_VERSION << item.url << item.dateTime << item.title; out << data; } tempFile.close(); if (saveAll) { if (historyFile.exists() && !historyFile.remove()) qWarning() << "History: error removing old history." << historyFile.errorString(); if (!tempFile.rename(historyFile.fileName())) qWarning() << "History: error moving new history over old." << tempFile.errorString() << historyFile.fileName(); } m_lastSavedUrl = m_history.value(0).url; } HistoryModel::HistoryModel(HistoryManager *history, QObject *parent) : QAbstractTableModel(parent) , m_history(history) { Q_ASSERT(m_history); connect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset())); connect(m_history, SIGNAL(entryRemoved(HistoryItem)), this, SLOT(entryRemoved(HistoryItem))); connect(m_history, SIGNAL(entryAdded(HistoryItem)), this, SLOT(entryAdded(HistoryItem))); connect(m_history, SIGNAL(entryUpdated(int)), this, SLOT(entryUpdated(int))); } void HistoryModel::historyReset() { beginResetModel(); m_history->history().clear(); endResetModel(); } void HistoryModel::entryAdded(const HistoryItem &item) { beginInsertRows(QModelIndex(), 0, 0); m_history->history().prepend(item); endInsertRows(); } void HistoryModel::entryRemoved(const HistoryItem &item) { int index = m_history->history().indexOf(item); Q_ASSERT(index > -1); beginRemoveRows(QModelIndex(),index, index); m_history->history().takeAt(index); endRemoveRows(); } void HistoryModel::entryUpdated(int offset) { QModelIndex idx = index(offset, 0); emit dataChanged(idx, idx); } QVariant HistoryModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case 0: return tr("Title"); case 1: return tr("Address"); } } return QAbstractTableModel::headerData(section, orientation, role); } QVariant HistoryModel::data(const QModelIndex &index, int role) const { QList<HistoryItem> lst = m_history->history(); if (index.row() < 0 || index.row() >= lst.size()) return QVariant(); const HistoryItem &item = lst.at(index.row()); switch (role) { case DateTimeRole: return item.dateTime; case DateRole: return item.dateTime.date(); case UrlRole: return QUrl(item.url); case UrlStringRole: return item.url; case Qt::DisplayRole: case Qt::EditRole: { switch (index.column()) { case 0: // when there is no title try to generate one from the url if (item.title.isEmpty()) { QString page = QFileInfo(QUrl(item.url).path()).fileName(); if (!page.isEmpty()) return page; return item.url; } return item.title; case 1: return item.url; } } case Qt::DecorationRole: if (index.column() == 0) { return BrowserApplication::instance()->icon(item.url); } } return QVariant(); } int HistoryModel::columnCount(const QModelIndex &parent) const { return (parent.isValid()) ? 0 : 2; } int HistoryModel::rowCount(const QModelIndex &parent) const { return (parent.isValid()) ? 0 : m_history->history().count(); } bool HistoryModel::removeRows(int row, int count, const QModelIndex &parent) { if (parent.isValid()) return false; int lastRow = row + count - 1; beginRemoveRows(parent, row, lastRow); QList<HistoryItem> &lst = m_history->history(); for (int i = lastRow; i >= row; --i) lst.removeAt(i); endRemoveRows(); return true; } #define MOVEDROWS 15 /* Maps the first bunch of items of the source model to the root */ HistoryMenuModel::HistoryMenuModel(HistoryTreeModel *sourceModel, QObject *parent) : QAbstractProxyModel(parent) , m_treeModel(sourceModel) { setSourceModel(sourceModel); } int HistoryMenuModel::bumpedRows() const { QModelIndex first = m_treeModel->index(0, 0); if (!first.isValid()) return 0; return qMin(m_treeModel->rowCount(first), MOVEDROWS); } int HistoryMenuModel::columnCount(const QModelIndex &parent) const { return m_treeModel->columnCount(mapToSource(parent)); } int HistoryMenuModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; if (!parent.isValid()) { int folders = sourceModel()->rowCount(); int bumpedItems = bumpedRows(); if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(sourceModel()->index(0, 0))) --folders; return bumpedItems + folders; } if (parent.internalId() == quintptr(-1)) { if (parent.row() < bumpedRows()) return 0; } QModelIndex idx = mapToSource(parent); int defaultCount = sourceModel()->rowCount(idx); if (idx == sourceModel()->index(0, 0)) return defaultCount - bumpedRows(); return defaultCount; } QModelIndex HistoryMenuModel::mapFromSource(const QModelIndex &sourceIndex) const { // currently not used or autotested Q_ASSERT(false); int sr = m_treeModel->mapToSource(sourceIndex).row(); return createIndex(sourceIndex.row(), sourceIndex.column(), sr); } QModelIndex HistoryMenuModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid()) return QModelIndex(); if (proxyIndex.internalId() == quintptr(-1)) { int bumpedItems = bumpedRows(); if (proxyIndex.row() < bumpedItems) return m_treeModel->index(proxyIndex.row(), proxyIndex.column(), m_treeModel->index(0, 0)); if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(m_treeModel->index(0, 0))) --bumpedItems; return m_treeModel->index(proxyIndex.row() - bumpedItems, proxyIndex.column()); } QModelIndex historyIndex = m_treeModel->sourceModel()->index(proxyIndex.internalId(), proxyIndex.column()); QModelIndex treeIndex = m_treeModel->mapFromSource(historyIndex); return treeIndex; } QModelIndex HistoryMenuModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || column >= columnCount(parent) || parent.column() > 0) return QModelIndex(); if (!parent.isValid()) return createIndex(row, column, quintptr(-1)); QModelIndex treeIndexParent = mapToSource(parent); int bumpedItems = 0; if (treeIndexParent == m_treeModel->index(0, 0)) bumpedItems = bumpedRows(); QModelIndex treeIndex = m_treeModel->index(row + bumpedItems, column, treeIndexParent); QModelIndex historyIndex = m_treeModel->mapToSource(treeIndex); int historyRow = historyIndex.row(); if (historyRow == -1) historyRow = treeIndex.row(); return createIndex(row, column, historyRow); } QModelIndex HistoryMenuModel::parent(const QModelIndex &index) const { int offset = index.internalId(); if (offset == -1 || !index.isValid()) return QModelIndex(); QModelIndex historyIndex = m_treeModel->sourceModel()->index(index.internalId(), 0); QModelIndex treeIndex = m_treeModel->mapFromSource(historyIndex); QModelIndex treeIndexParent = treeIndex.parent(); int sr = m_treeModel->mapToSource(treeIndexParent).row(); int bumpedItems = bumpedRows(); if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(sourceModel()->index(0, 0))) --bumpedItems; return createIndex(bumpedItems + treeIndexParent.row(), treeIndexParent.column(), sr); } HistoryMenu::HistoryMenu(QWidget *parent) : ModelMenu(parent) , m_history(0) { connect(this, SIGNAL(activated(QModelIndex)), this, SLOT(activated(QModelIndex))); setHoverRole(HistoryModel::UrlStringRole); } void HistoryMenu::activated(const QModelIndex &index) { emit openUrl(index.data(HistoryModel::UrlRole).toUrl()); } bool HistoryMenu::prePopulated() { if (!m_history) { m_history = BrowserApplication::historyManager(); m_historyMenuModel = new HistoryMenuModel(m_history->historyTreeModel(), this); setModel(m_historyMenuModel); } // initial actions for (int i = 0; i < m_initialActions.count(); ++i) addAction(m_initialActions.at(i)); if (!m_initialActions.isEmpty()) addSeparator(); setFirstSeparator(m_historyMenuModel->bumpedRows()); return false; } void HistoryMenu::postPopulated() { if (m_history->history().count() > 0) addSeparator(); QAction *showAllAction = new QAction(tr("Show All History"), this); connect(showAllAction, SIGNAL(triggered()), this, SLOT(showHistoryDialog())); addAction(showAllAction); QAction *clearAction = new QAction(tr("Clear History"), this); connect(clearAction, SIGNAL(triggered()), m_history, SLOT(clear())); addAction(clearAction); } void HistoryMenu::showHistoryDialog() { HistoryDialog *dialog = new HistoryDialog(this); connect(dialog, SIGNAL(openUrl(QUrl)), this, SIGNAL(openUrl(QUrl))); dialog->show(); } void HistoryMenu::setInitialActions(QList<QAction*> actions) { m_initialActions = actions; for (int i = 0; i < m_initialActions.count(); ++i) addAction(m_initialActions.at(i)); } TreeProxyModel::TreeProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { setSortRole(HistoryModel::DateTimeRole); setFilterCaseSensitivity(Qt::CaseInsensitive); } bool TreeProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } HistoryDialog::HistoryDialog(QWidget *parent, HistoryManager *setHistory) : QDialog(parent) { HistoryManager *history = setHistory; if (!history) history = BrowserApplication::historyManager(); setupUi(this); tree->setUniformRowHeights(true); tree->setSelectionBehavior(QAbstractItemView::SelectRows); tree->setTextElideMode(Qt::ElideMiddle); QAbstractItemModel *model = history->historyFilterModel(); TreeProxyModel *proxyModel = new TreeProxyModel(this); connect(search, SIGNAL(textChanged(QString)), proxyModel, SLOT(setFilterFixedString(QString))); connect(removeButton, SIGNAL(clicked()), tree, SLOT(removeOne())); connect(removeAllButton, SIGNAL(clicked()), tree, SLOT(removeAll())); proxyModel->setSourceModel(model); proxyModel->setFilterKeyColumn(1); tree->setModel(proxyModel); tree->setSortingEnabled(true); tree->setExpanded(proxyModel->index(0, 0), true); tree->setAlternatingRowColors(true); QFontMetrics fm(font()); int header = fm.width(QLatin1Char('m')) * 40; tree->header()->resizeSection(0, header); tree->header()->setStretchLastSection(true); connect(tree, SIGNAL(activated(QModelIndex)), this, SLOT(open())); tree->setContextMenuPolicy(Qt::CustomContextMenu); connect(tree, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(customContextMenuRequested(QPoint))); } void HistoryDialog::customContextMenuRequested(const QPoint &pos) { QMenu menu; QModelIndex index = tree->indexAt(pos); index = index.sibling(index.row(), 0); if (index.isValid() && !tree->model()->hasChildren(index)) { menu.addAction(tr("Open"), this, SLOT(open())); menu.addSeparator(); menu.addAction(tr("Copy"), this, SLOT(copy())); } menu.addAction(tr("Delete"), tree, SLOT(removeOne())); menu.exec(QCursor::pos()); } void HistoryDialog::open() { QModelIndex index = tree->currentIndex(); if (!index.parent().isValid()) return; emit openUrl(index.data(HistoryModel::UrlRole).toUrl()); } void HistoryDialog::copy() { QModelIndex index = tree->currentIndex(); if (!index.parent().isValid()) return; QString url = index.data(HistoryModel::UrlStringRole).toString(); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(url); } HistoryFilterModel::HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent) : QAbstractProxyModel(parent), m_loaded(false) { setSourceModel(sourceModel); } void HistoryFilterModel::setSourceModel(QAbstractItemModel *newSourceModel) { beginResetModel(); if (sourceModel()) { disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(sourceDataChanged(QModelIndex,QModelIndex))); disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); } QAbstractProxyModel::setSourceModel(newSourceModel); if (sourceModel()) { connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); connect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(sourceDataChanged(QModelIndex,QModelIndex))); connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); } load(); endResetModel(); } void HistoryFilterModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight)); } QVariant HistoryFilterModel::headerData(int section, Qt::Orientation orientation, int role) const { return sourceModel()->headerData(section, orientation, role); } void HistoryFilterModel::sourceReset() { beginResetModel(); load(); endResetModel(); } int HistoryFilterModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return m_historyHash.count(); } int HistoryFilterModel::columnCount(const QModelIndex &parent) const { return (parent.isValid()) ? 0 : 2; } QModelIndex HistoryFilterModel::mapToSource(const QModelIndex &proxyIndex) const { int sourceRow = sourceModel()->rowCount() - proxyIndex.internalId(); return sourceModel()->index(sourceRow, proxyIndex.column()); } QModelIndex HistoryFilterModel::mapFromSource(const QModelIndex &sourceIndex) const { QString url = sourceIndex.data(HistoryModel::UrlStringRole).toString(); if (!m_historyHash.contains(url)) return QModelIndex(); // This can be done in a binary search, but we can't use qBinary find // because it can't take: qBinaryFind(m_sourceRow.end(), m_sourceRow.begin(), v); // so if this is a performance bottlneck then convert to binary search, until then // the cleaner/easier to read code wins the day. int realRow = -1; int sourceModelRow = sourceModel()->rowCount() - sourceIndex.row(); for (int i = 0; i < m_sourceRow.count(); ++i) { if (m_sourceRow.at(i) == sourceModelRow) { realRow = i; break; } } if (realRow == -1) return QModelIndex(); return createIndex(realRow, sourceIndex.column(), sourceModel()->rowCount() - sourceIndex.row()); } QModelIndex HistoryFilterModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount(parent)) return QModelIndex(); return createIndex(row, column, m_sourceRow[row]); } QModelIndex HistoryFilterModel::parent(const QModelIndex &) const { return QModelIndex(); } void HistoryFilterModel::load() const { m_sourceRow.clear(); m_historyHash.clear(); m_historyHash.reserve(sourceModel()->rowCount()); for (int i = 0; i < sourceModel()->rowCount(); ++i) { QModelIndex idx = sourceModel()->index(i, 0); QString url = idx.data(HistoryModel::UrlStringRole).toString(); if (!m_historyHash.contains(url)) { m_sourceRow.append(sourceModel()->rowCount() - i); m_historyHash[url] = sourceModel()->rowCount() - i; } } } void HistoryFilterModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) { Q_ASSERT(start == end && start == 0); Q_UNUSED(end); QModelIndex idx = sourceModel()->index(start, 0, parent); QString url = idx.data(HistoryModel::UrlStringRole).toString(); if (m_historyHash.contains(url)) { int sourceRow = sourceModel()->rowCount() - m_historyHash[url]; int realRow = mapFromSource(sourceModel()->index(sourceRow, 0)).row(); beginRemoveRows(QModelIndex(), realRow, realRow); m_sourceRow.removeAt(realRow); m_historyHash.remove(url); endRemoveRows(); } beginInsertRows(QModelIndex(), 0, 0); m_historyHash.insert(url, sourceModel()->rowCount() - start); m_sourceRow.insert(0, sourceModel()->rowCount()); endInsertRows(); } void HistoryFilterModel::sourceRowsRemoved(const QModelIndex &, int start, int end) { Q_UNUSED(start); Q_UNUSED(end); sourceReset(); } /* Removing a continuous block of rows will remove filtered rows too as this is the users intention. */ bool HistoryFilterModel::removeRows(int row, int count, const QModelIndex &parent) { if (row < 0 || count <= 0 || row + count > rowCount(parent) || parent.isValid()) return false; int lastRow = row + count - 1; disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); beginRemoveRows(parent, row, lastRow); int oldCount = rowCount(); int start = sourceModel()->rowCount() - m_sourceRow.value(row); int end = sourceModel()->rowCount() - m_sourceRow.value(lastRow); sourceModel()->removeRows(start, end - start + 1); endRemoveRows(); connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); m_loaded = false; if (oldCount - count != rowCount()) { beginResetModel(); endResetModel(); } return true; } HistoryCompletionModel::HistoryCompletionModel(QObject *parent) : QAbstractProxyModel(parent) { } QVariant HistoryCompletionModel::data(const QModelIndex &index, int role) const { if (sourceModel() && (role == Qt::EditRole || role == Qt::DisplayRole) && index.isValid()) { QModelIndex idx = mapToSource(index); idx = idx.sibling(idx.row(), 1); QString urlString = idx.data(HistoryModel::UrlStringRole).toString(); if (index.row() % 2) { QUrl url = urlString; QString s = url.toString(QUrl::RemoveScheme | QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); return s.mid(2); // strip // from the front } return urlString; } return QAbstractProxyModel::data(index, role); } int HistoryCompletionModel::rowCount(const QModelIndex &parent) const { return (parent.isValid() || !sourceModel()) ? 0 : sourceModel()->rowCount(parent) * 2; } int HistoryCompletionModel::columnCount(const QModelIndex &parent) const { return (parent.isValid()) ? 0 : 1; } QModelIndex HistoryCompletionModel::mapFromSource(const QModelIndex &sourceIndex) const { int row = sourceIndex.row() * 2; return index(row, sourceIndex.column()); } QModelIndex HistoryCompletionModel::mapToSource(const QModelIndex &proxyIndex) const { if (!sourceModel()) return QModelIndex(); int row = proxyIndex.row() / 2; return sourceModel()->index(row, proxyIndex.column()); } QModelIndex HistoryCompletionModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount(parent)) return QModelIndex(); return createIndex(row, column); } QModelIndex HistoryCompletionModel::parent(const QModelIndex &) const { return QModelIndex(); } void HistoryCompletionModel::setSourceModel(QAbstractItemModel *newSourceModel) { if (sourceModel()) { disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceReset())); disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceReset())); } QAbstractProxyModel::setSourceModel(newSourceModel); if (newSourceModel) { connect(newSourceModel, SIGNAL(modelReset()), this, SLOT(sourceReset())); connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceReset())); connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceReset())); } beginResetModel(); endResetModel(); } void HistoryCompletionModel::sourceReset() { beginResetModel(); endResetModel(); } HistoryTreeModel::HistoryTreeModel(QAbstractItemModel *sourceModel, QObject *parent) : QAbstractProxyModel(parent) { setSourceModel(sourceModel); } QVariant HistoryTreeModel::headerData(int section, Qt::Orientation orientation, int role) const { return sourceModel()->headerData(section, orientation, role); } QVariant HistoryTreeModel::data(const QModelIndex &index, int role) const { if ((role == Qt::EditRole || role == Qt::DisplayRole)) { int start = index.internalId(); if (start == 0) { int offset = sourceDateRow(index.row()); if (index.column() == 0) { QModelIndex idx = sourceModel()->index(offset, 0); QDate date = idx.data(HistoryModel::DateRole).toDate(); if (date == QDate::currentDate()) return tr("Earlier Today"); return date.toString(QLatin1String("dddd, MMMM d, yyyy")); } if (index.column() == 1) { return tr("%1 items").arg(rowCount(index.sibling(index.row(), 0))); } } } if (role == Qt::DecorationRole && index.column() == 0 && !index.parent().isValid()) return QIcon(QLatin1String(":history.png")); if (role == HistoryModel::DateRole && index.column() == 0 && index.internalId() == 0) { int offset = sourceDateRow(index.row()); QModelIndex idx = sourceModel()->index(offset, 0); return idx.data(HistoryModel::DateRole); } return QAbstractProxyModel::data(index, role); } int HistoryTreeModel::columnCount(const QModelIndex &parent) const { return sourceModel()->columnCount(mapToSource(parent)); } int HistoryTreeModel::rowCount(const QModelIndex &parent) const { if ( parent.internalId() != 0 || parent.column() > 0 || !sourceModel()) return 0; // row count OF dates if (!parent.isValid()) { if (!m_sourceRowCache.isEmpty()) return m_sourceRowCache.count(); QDate currentDate; int rows = 0; int totalRows = sourceModel()->rowCount(); for (int i = 0; i < totalRows; ++i) { QDate rowDate = sourceModel()->index(i, 0).data(HistoryModel::DateRole).toDate(); if (rowDate != currentDate) { m_sourceRowCache.append(i); currentDate = rowDate; ++rows; } } Q_ASSERT(m_sourceRowCache.count() == rows); return rows; } // row count FOR a date int start = sourceDateRow(parent.row()); int end = sourceDateRow(parent.row() + 1); return (end - start); } // Translate the top level date row into the offset where that date starts int HistoryTreeModel::sourceDateRow(int row) const { if (row <= 0) return 0; if (m_sourceRowCache.isEmpty()) rowCount(QModelIndex()); if (row >= m_sourceRowCache.count()) { if (!sourceModel()) return 0; return sourceModel()->rowCount(); } return m_sourceRowCache.at(row); } QModelIndex HistoryTreeModel::mapToSource(const QModelIndex &proxyIndex) const { int offset = proxyIndex.internalId(); if (offset == 0) return QModelIndex(); int startDateRow = sourceDateRow(offset - 1); return sourceModel()->index(startDateRow + proxyIndex.row(), proxyIndex.column()); } QModelIndex HistoryTreeModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || column >= columnCount(parent) || parent.column() > 0) return QModelIndex(); if (!parent.isValid()) return createIndex(row, column); return createIndex(row, column, parent.row() + 1); } QModelIndex HistoryTreeModel::parent(const QModelIndex &index) const { int offset = index.internalId(); if (offset == 0 || !index.isValid()) return QModelIndex(); return createIndex(offset - 1, 0); } bool HistoryTreeModel::hasChildren(const QModelIndex &parent) const { QModelIndex grandparent = parent.parent(); if (!grandparent.isValid()) return true; return false; } Qt::ItemFlags HistoryTreeModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; } bool HistoryTreeModel::removeRows(int row, int count, const QModelIndex &parent) { if (row < 0 || count <= 0 || row + count > rowCount(parent)) return false; if (parent.isValid()) { // removing pages int offset = sourceDateRow(parent.row()); return sourceModel()->removeRows(offset + row, count); } else { // removing whole dates for (int i = row + count - 1; i >= row; --i) { QModelIndex dateParent = index(i, 0); int offset = sourceDateRow(dateParent.row()); if (!sourceModel()->removeRows(offset, rowCount(dateParent))) return false; } } return true; } void HistoryTreeModel::setSourceModel(QAbstractItemModel *newSourceModel) { beginResetModel(); if (sourceModel()) { disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset())); disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); disconnect(sourceModel(), &QAbstractItemModel::dataChanged, this, &HistoryTreeModel::sourceDataChanged); } QAbstractProxyModel::setSourceModel(newSourceModel); if (newSourceModel) { connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset())); connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); connect(sourceModel(), &QAbstractItemModel::dataChanged, this, &HistoryTreeModel::sourceDataChanged); } endResetModel(); } void HistoryTreeModel::sourceReset() { beginResetModel(); m_sourceRowCache.clear(); endResetModel(); } void HistoryTreeModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) { Q_UNUSED(parent); // Avoid warnings when compiling release Q_ASSERT(!parent.isValid()); if (start != 0 || start != end) { beginResetModel(); m_sourceRowCache.clear(); endResetModel(); return; } m_sourceRowCache.clear(); QModelIndex treeIndex = mapFromSource(sourceModel()->index(start, 0)); QModelIndex treeParent = treeIndex.parent(); if (rowCount(treeParent) == 1) { beginInsertRows(QModelIndex(), 0, 0); endInsertRows(); } else { beginInsertRows(treeParent, treeIndex.row(), treeIndex.row()); endInsertRows(); } } QModelIndex HistoryTreeModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) return QModelIndex(); if (m_sourceRowCache.isEmpty()) rowCount(QModelIndex()); QList<int>::iterator it; it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), sourceIndex.row()); if (*it != sourceIndex.row()) --it; int dateRow = qMax(0, it - m_sourceRowCache.begin()); int row = sourceIndex.row() - m_sourceRowCache.at(dateRow); return createIndex(row, sourceIndex.column(), dateRow + 1); } void HistoryTreeModel::sourceRowsRemoved(const QModelIndex &parent, int start, int end) { Q_UNUSED(parent); // Avoid warnings when compiling release Q_ASSERT(!parent.isValid()); if (m_sourceRowCache.isEmpty()) return; for (int i = end; i >= start;) { QList<int>::iterator it; it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), i); // playing it safe if (it == m_sourceRowCache.end()) { beginResetModel(); m_sourceRowCache.clear(); endResetModel(); return; } if (*it != i) --it; int row = qMax(0, it - m_sourceRowCache.begin()); int offset = m_sourceRowCache[row]; QModelIndex dateParent = index(row, 0); // If we can remove all the rows in the date do that and skip over them int rc = rowCount(dateParent); if (i - rc + 1 == offset && start <= i - rc + 1) { beginRemoveRows(QModelIndex(), row, row); m_sourceRowCache.removeAt(row); i -= rc + 1; } else { beginRemoveRows(dateParent, i - offset, i - offset); ++row; --i; } for (int j = row; j < m_sourceRowCache.count(); ++j) --m_sourceRowCache[j]; endRemoveRows(); } } void HistoryTreeModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> roles) { emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles); }