history.cpp Example File
demos/browser/history.cpp
#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();
}
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();
}
}