|  | Home · All Classes · Main Classes · Grouped Classes · Modules · Functions |

# history.cpp Example File
demos/browser/history.cpp

     /****************************************************************************
     **
     ** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved.
     **
     ** This file is part of the documentation of the Qt Toolkit.
     **
     ** This file may be used under the terms of the GNU General Public
    ** License versions 2.0 or 3.0 as published by the Free Software
    ** Foundation and appearing in the files LICENSE.GPL2 and LICENSE.GPL3
    ** included in the packaging of this file.  Alternatively you may (at
    ** your option) use any later version of the GNU General Public
    ** License if such license has been publicly approved by Trolltech ASA
    ** (or its successors, if any) and the KDE Free Qt Foundation. In
    ** addition, as a special exception, Trolltech gives you certain
    ** additional rights. These rights are described in the Trolltech GPL
    ** Exception version 1.2, which can be found at
    ** http://www.trolltech.com/products/qt/gplexception/ and in the file
    ** GPL_EXCEPTION.txt in this package.
    **
    ** Please review the following information to ensure GNU General
    ** Public Licensing requirements will be met:
    ** http://trolltech.com/products/qt/licenses/licensing/opensource/. If
    ** you are unsure which license is appropriate for your use, please
    ** review the following information:
    ** http://trolltech.com/products/qt/licenses/licensing/licensingoverview
    ** or contact the sales department at sales@trolltech.com.
    **
    ** In addition, as a special exception, Trolltech, as the sole
    ** copyright holder for Qt Designer, grants users of the Qt/Eclipse
    ** Integration plug-in the right for the Qt/Eclipse Integration to
    ** link to functionality provided by Qt Designer and its related
    ** libraries.
    **
    ** This file is provided "AS IS" with NO WARRANTY OF ANY KIND,
    ** INCLUDING THE WARRANTIES OF DESIGN, MERCHANTABILITY AND FITNESS FOR
    ** A PARTICULAR PURPOSE. Trolltech reserves all rights not expressly
    ** granted herein.
     **
     ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
     ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
     **
     ****************************************************************************/

     #include "history.h"
     #include "autosave.h"

     #include <QtCore/QSettings>
     #include <QtCore/QFile>
     #include <QtCore/QFileInfo>
     #include <QtCore/QTextStream>
     #include <QtCore/QTemporaryFile>
     #include <QtAlgorithms>
     #include <QtGui/QStyle>
     #include <QtGui/QHeaderView>
     #include <QtGui/QDesktopServices>
     #include <QtWebKit/QWebSettings>
     #include <qdebug.h>

     static const unsigned int HISTORY_VERSION = 23;

     History::History(QObject *parent)
         : QWebHistoryInterface(parent)
         , m_saveTimer(new AutoSave(this))
         , m_historyLimit(30)
     {
         m_expiredTimer.setSingleShot(true);
         connect(&m_expiredTimer, SIGNAL(timeout()),
                 this, SLOT(checkForExpired()));
         connect(this, SIGNAL(entryAdded(const HistoryItem &)),
                 m_saveTimer, SLOT(changed()));
         load();

         m_historyModel = new HistoryModel(this, this);
         m_historyFilterModel = new HistoryFilterModel(m_historyModel, this);
         m_historyTreeModel = new HistoryTreeModel(m_historyFilterModel, this);
     }

     History::~History()
     {
         m_saveTimer->saveNow();
     }

     QList<HistoryItem> History::history() const
     {
         return m_history;
     }

     bool History::historyContains(const QString &url) const
     {
         return m_historyFilterModel->historyContains(url);
     }

     void History::addHistoryEntry(const QString &url)
     {
         QUrl cleanUrl(url);
         cleanUrl.setPassword(QString());
         cleanUrl.setHost(cleanUrl.host().toLower());
         HistoryItem item(cleanUrl.toString(), QDateTime::currentDateTime());
         addHistoryItem(item);
     }

     void History::setHistory(const QList<HistoryItem> &history, bool loaded)
     {
         m_history = history;

         // verify sorted by date
         if (!loaded)
             qSort(m_history.begin(), m_history.end());

         checkForExpired();

         if (loaded) {
             m_lastSavedUrl = m_history.value(0).url;
         } else {
             m_lastSavedUrl = QString();
             m_saveTimer->changed();
         }
         emit historyReset();
     }

     QAbstractItemModel *History::historyModel() const
     {
         return m_historyModel;
     }

     QAbstractItemModel *History::historyFilterModel() const
     {
         return m_historyFilterModel;
     }

     QAbstractItemModel *History::historyTreeModel() const
     {
         return m_historyTreeModel;
     }

     void History::checkForExpired()
     {
         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;
             HistoryItem item = m_history.takeLast();
             emit entryRemoved(item);
             // remove from saved file also
             m_lastSavedUrl = QString();
         }

         if (nextTimeout > 0)
             m_expiredTimer.start(nextTimeout * 1000);
     }

     void History::addHistoryItem(const HistoryItem &item)
     {
         m_history.prepend(item);
         emit entryAdded(item);
         if (m_history.count() == 1)
             checkForExpired();
     }

     void History::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->changed();
                 emit entryUpdated(i);
                 break;
             }
         }
     }

     int History::historyLimit() const
     {
         return m_historyLimit;
     }

     void History::setHistoryLimit(int limit)
     {
         if (m_historyLimit == limit)
             return;
         m_historyLimit = limit;
         checkForExpired();
         m_saveTimer->changed();
     }

     void History::clear()
     {
         m_history.clear();
         m_lastSavedUrl = QString();
         m_saveTimer->changed();
         m_saveTimer->saveNow();
         historyReset();
     }

     void History::load()
     {
         // load settings
         QSettings settings;
         settings.beginGroup("history");
         m_historyLimit = settings.value(QLatin1String("historyLimit"), 30).toInt();

         QFile historyFile(QDesktopServices::storageLocation(QDesktopServices::DataLocation) + "/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 sort = false;
         HistoryItem last;
         QByteArray data;
         while (!historyFile.atEnd()) {
             in >> data;
             QDataStream stream(&data, 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()
                 || item == last)
                 continue;

             if (!sort && !list.isEmpty() && last < item)
                 sort = true;

             list.prepend(item);
             last = item;
         }
         if (sort)
             qSort(list.begin(), list.end());

         setHistory(list, true);

         // If we had to sort re-write the whole history sorted
         if (sort) {
             m_lastSavedUrl = QString();
             m_saveTimer->changed();
         }
     }

     void History::save()
     {
         QSettings settings;
         settings.beginGroup("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;

         QFile historyFile(QDesktopServices::storageLocation(QDesktopServices::DataLocation) + "/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;
         }

         if (saveAll) {
             if (historyFile.exists() && !historyFile.remove())
                 qWarning() << "History: error removing old history.";
             if (!tempFile.rename(historyFile.fileName()))
                 qWarning() << "History: error moving new history over old.";
         }
         m_lastSavedUrl = m_history.value(0).url;
     }

     HistoryModel::HistoryModel(History *h, QObject *parent)
         : QAbstractTableModel(parent), history(h)
     {
         if (history) {
             connect(history, SIGNAL(historyReset()), this, SLOT(historyReset()));
             connect(history, SIGNAL(entryRemoved(const HistoryItem &)),
                     this, SLOT(historyReset()));

             connect(history, SIGNAL(entryAdded(const HistoryItem &)),
                     this, SLOT(entryAdded()));
             connect(history, SIGNAL(entryUpdated(int)),
                     this, SLOT(entryUpdated(int)));
         }
     }

     void HistoryModel::historyReset()
     {
         reset();
     }

     void HistoryModel::entryAdded()
     {
         beginInsertRows(QModelIndex(), 0, 0);
         endInsertRows();
     }

     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 = 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:
                     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) {
                 QIcon icon = QWebSettings::iconForUrl(item.url);
                 if (!icon.isNull())
                     return icon;
                 if (defaultIcon.isNull())
                     defaultIcon = QApplication::style()->standardIcon(QStyle::SP_FileIcon);
                 return defaultIcon;
             }
         }
         return QVariant();
     }

     int HistoryModel::columnCount(const QModelIndex &parent) const
     {
         return (parent.isValid()) ? 0 : 2;
     }

     int HistoryModel::rowCount(const QModelIndex &parent) const
     {
         return (parent.isValid() || !history) ? 0 : 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 = history->history();
         for (int i = lastRow; i >= row; --i)
             lst.removeAt(i);
         disconnect(history, SIGNAL(historyReset()), this, SLOT(historyReset()));
         history->setHistory(lst);
         connect(history, SIGNAL(historyReset()), this, SLOT(historyReset()));
         endRemoveRows();
         return true;
     }

     HistoryMenu::HistoryMenu(History *history, QWidget *parent)
         : ModelMenu(history->historyTreeModel(), parent),
          m_history(history)
     {
         connect(this, SIGNAL(activated(const QModelIndex &)),
                 this, SLOT(activated(const QModelIndex &)));
     }

     void HistoryMenu::activated(const QModelIndex &index)
     {
         emit openUrl(index.data(HistoryModel::UrlRole).toUrl());
     }

     bool HistoryMenu::prePopulated()
     {
         // initial actions
         for (int i = 0; i < initialActions.count(); ++i)
             addAction(initialActions.at(i));
         if (!initialActions.isEmpty())
             addSeparator();

         createMenu(model()->index(0, 0), 20, this);
         return true;
     }

     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(m_history, this);
         connect(dialog, SIGNAL(openUrl(const QUrl&)), this, SIGNAL(openUrl(const QUrl&)));
         dialog->show();
     }

     void HistoryMenu::setInitialActions(QList<QAction*> actions)
     {
         initialActions = actions;
     }

     TreeProxyModel::TreeProxyModel(QObject * parent) : QSortFilterProxyModel(parent)
     {
         setSortRole(HistoryModel::DateTimeRole);
         setFilterCaseSensitivity(Qt::CaseInsensitive);
     }

     bool TreeProxyModel::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const
     {
         if (!source_parent.isValid())
             return true;
         return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
     }

     HistoryDialog::HistoryDialog(History *history, QWidget *parent) : QDialog(parent)
     {
         setupUi(this);
         tree->setUniformRowHeights(true);
         tree->setSelectionBehavior(QAbstractItemView::SelectRows);
         tree->setTextElideMode(Qt::ElideMiddle);
         QAbstractItemModel *model = history->historyTreeModel();
         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()), history, SLOT(clear()));
         proxyModel->setSourceModel(model);
         tree->setModel(proxyModel);
         tree->setExpanded(proxyModel->index(0, 0), true);
         tree->setAlternatingRowColors(true);
         QFontMetrics fm(font());
         int header = fm.width("m") * 40;
         tree->header()->resizeSection(0, header);
         tree->header()->setStretchLastSection(true);
         connect(tree, SIGNAL(activated(const QModelIndex&)),
                 this, SLOT(open()));
         tree->setContextMenuPolicy(Qt::CustomContextMenu);
         connect(tree, SIGNAL(customContextMenuRequested(const QPoint &)),
                 this, SLOT(customContextMenuRequested(const 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("Delete"), tree, SLOT(removeOne()));
         menu.exec(QCursor::pos());
     }

     void HistoryDialog::open()
     {
         QModelIndex index = tree->currentIndex();
         if (!index.parent().isValid())
             return;
         emit openUrl(index.sibling(index.row(), 1).data(HistoryModel::UrlRole).toUrl());
     }

     HistoryFilterModel::HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent)
         : QAbstractProxyModel(parent),
         loaded(false)
     {
         setSourceModel(sourceModel);
     }

     QVariant HistoryFilterModel::data(const QModelIndex &index, int role) const
     {
         return QAbstractProxyModel::data(index, role);
     }

     void HistoryFilterModel::setSourceModel(QAbstractItemModel *newSourceModel)
     {
         if (sourceModel()) {
             disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
             disconnect(sourceModel(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)),
                        this, SLOT(dataChanged(const QModelIndex &, const QModelIndex &)));
             disconnect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsInserted(const QModelIndex &, int, int)));
             disconnect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsRemoved(const QModelIndex &, int, int)));
         }

         QAbstractProxyModel::setSourceModel(newSourceModel);

         if (sourceModel()) {
             loaded = false;
             connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
             connect(sourceModel(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)),
                        this, SLOT(sourceDataChanged(const QModelIndex &, const QModelIndex &)));
             connect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsInserted(const QModelIndex &, int, int)));
             connect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsRemoved(const QModelIndex &, int, int)));
         }
     }

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

     int HistoryFilterModel::historyLocation(const QString &url) const
     {
         load();
         if (!m_historyHash.contains(url))
             return 0;
         return sourceModel()->rowCount() - m_historyHash.value(url);
     }

     void HistoryFilterModel::sourceReset()
     {
         loaded = false;
         reset();
     }

     int HistoryFilterModel::rowCount(const QModelIndex &parent) const
     {
         load();
         if (parent.isValid())
             return 0;
         return m_historyHash.count();
     }

     int HistoryFilterModel::columnCount(const QModelIndex &parent) const
     {
         return sourceModel()->columnCount(parent);
     }

     QModelIndex HistoryFilterModel::mapToSource(const QModelIndex &proxyIndex) const
     {
         load();
         int sourceRow = sourceModel()->rowCount() - proxyIndex.internalId();
         return sourceModel()->index(sourceRow, proxyIndex.column());
     }

     QModelIndex HistoryFilterModel::mapFromSource(const QModelIndex &sourceIndex) const
     {
         load();
         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[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
     {
         load();
         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
     {
         if (loaded)
             return;
         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;
             }
         }
         loaded = true;
     }

     void HistoryFilterModel::sourceRowsInserted(const QModelIndex &parent, int start, int end)
     {
         Q_ASSERT(start == end && start == 0);
         Q_UNUSED(end);
         if (!loaded)
             return;
         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 hidden 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(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsRemoved(const 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(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsRemoved(const QModelIndex &, int, int)));
         loaded = false;
         if (oldCount - count != rowCount())
             reset();
         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);
             if (index.row() % 2) {
                 QUrl url = idx.data(HistoryModel::UrlRole).toUrl();
                 QString s = url.toString(QUrl::RemoveScheme
                                          | QUrl::RemoveUserInfo
                                          | QUrl::StripTrailingSlash);
                 return s.mid(2);  // strip // from the front
             }
             return idx.data(role);
         }
         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, 0);
     }

     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(const QModelIndex &, int, int)),
                     this, SLOT(sourceReset()));
             disconnect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
                     this, SLOT(sourceReset()));
         }

         QAbstractProxyModel::setSourceModel(newSourceModel);

         if (newSourceModel) {
             connect(newSourceModel, SIGNAL(modelReset()), this, SLOT(sourceReset()));
             connect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
                     this, SLOT(sourceReset()));
             connect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
                     this, SLOT(sourceReset()));
         }

         reset();
     }

     void HistoryCompletionModel::sourceReset()
     {
         reset();
     }

     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 (sourceModel()) {
             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("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(":history.png");
         }

         return QAbstractProxyModel::data(index, role);
     }

     int HistoryTreeModel::columnCount(const QModelIndex &parent) const
     {
         if (!sourceModel())
             return 0;
         return sourceModel()->columnCount(mapToSource(parent));
     }

     int HistoryTreeModel::rowCount(const QModelIndex &parent) const
     {
         if (!sourceModel()
             || parent.internalId() != 0
             || parent.column() > 0)
             return 0;

         // row count OF dates
         if (!parent.isValid()) {
             if (!sourceRowCache.isEmpty())
                 return sourceRowCache.count() - 1;
             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) {
                     currentDate = rowDate;
                     ++rows;
                 }
             }
             sourceRowCache.append(0);
             while (sourceRowCache.count() <= rows)
                 sourceRowCache.append(-1);
             return rows;
         }

         // row count FOR a date
         int start = sourceDateRow(parent.row());
         int end = sourceDateRow(parent.row() + 1);
         return (end - start);
     }

     QModelIndex HistoryTreeModel::mapFromSource(const QModelIndex &sourceIndex) const
     {
         int dateRow = 0;
         int row = 0;
         QDate currentDate;
         for (int i = 0; i < sourceIndex.row(); ++i) {
             QDate rowDate = sourceModel()->index(i, 0).data(HistoryModel::DateRole).toDate();
             if (rowDate != currentDate) {
                 currentDate = rowDate;
                 row = 0;
                 ++dateRow;
             }
             ++row;
         }

         return createIndex(row, sourceIndex.column(), dateRow + 1);
     }

     // Translate the top level date row into the offset where that date starts
     int HistoryTreeModel::sourceDateRow(int row) const
     {
         if (!sourceModel() || row <= 0)
             return 0;

         if (!sourceRowCache.isEmpty()) {
             if (row >= sourceRowCache.count())
                 return sourceModel()->rowCount();
             if (sourceRowCache.at(row) != -1)
                 return sourceRowCache.at(row);
         }

         QDate currentDate;
         int currentRow = row - 1;
         int start = sourceDateRow(row - 1);
         int totalSourceRows = sourceModel()->rowCount();
         int i;
         for (i = start; i < totalSourceRows; ++i) {
             QDate rowDate = sourceModel()->index(i, 0).data(HistoryModel::DateRole).toDate();
             if (rowDate != currentDate) {
                 if (!currentDate.isNull())
                     ++currentRow;
                 if (currentRow == row)
                     break;
                 currentDate = rowDate;
             }
         }

         int offset = i;
         if (sourceRowCache.isEmpty())
             rowCount(QModelIndex());
         if (row < sourceRowCache.count())
             sourceRowCache[row] = offset;
         return offset;
     }

     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, 0);
         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, 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;
     }

     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());
             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());
                 sourceModel()->removeRows(offset, rowCount(dateParent));
             }
         }
         return true;
     }

     void HistoryTreeModel::setSourceModel(QAbstractItemModel *newSourceModel)
     {
         if (sourceModel()) {
             disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
             disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset()));
             disconnect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsInserted(const QModelIndex &, int, int)));
             disconnect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsRemoved(const QModelIndex &, int, int)));
         }

         QAbstractProxyModel::setSourceModel(newSourceModel);

         if (newSourceModel) {
             connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
             connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset()));
             connect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsInserted(const QModelIndex &, int, int)));
             connect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
                     this, SLOT(sourceRowsRemoved(const QModelIndex &, int, int)));
         }

         reset();
     }

     void HistoryTreeModel::sourceReset()
     {
         sourceRowCache.clear();
         reset();
     }

     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) {
             sourceRowCache.clear();
             reset();
             return;
         }

         sourceRowCache.clear();
         QModelIndex idx = mapFromSource(sourceModel()->index(start, 0));
         QModelIndex p = idx.parent();
         if (rowCount(p) == 1) {
             beginInsertRows(QModelIndex(), 0, 0);
             endInsertRows();
         } else {
             beginInsertRows(p, idx.row(), idx.row());
             endInsertRows();
         }
     }

     void HistoryTreeModel::sourceRowsRemoved(const QModelIndex &parent, int start, int end)
     {
         Q_UNUSED(parent); // Avoid warnings when compiling release
         Q_ASSERT(!parent.isValid());
         if (sourceRowCache.isEmpty())
             return;
         for (int i = end; i >= start;) {
             QList<int>::iterator it;
             it = qLowerBound(sourceRowCache.begin(), sourceRowCache.end(), i);
             if (*it != i)
                 --it;
             int row = qMax(0, it - sourceRowCache.begin());
             int offset = 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);
                 sourceRowCache.removeAt(row);
                 i -= rc + 1;
             } else {
                 beginRemoveRows(dateParent, i - offset, i - offset);
                 ++row;
                 --i;
             }
             for (int j = row; j < sourceRowCache.count(); ++j)
                 --sourceRowCache[j];
             endRemoveRows();
         }
     }