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 demonstration applications 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);
}

© 2017 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.