history.cpp Example File | Qt WebEngine 5.9

Qt 5.9 | Qt WebEngine | WebEngine Demo Browser Example | history.cpp Example File

Qt 5.9.9 Reference Documentation

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