diff options
Diffstat (limited to 'src/history')
-rw-r--r-- | src/history/autosaver.cpp | 96 | ||||
-rw-r--r-- | src/history/autosaver.h | 77 | ||||
-rw-r--r-- | src/history/historymanager.cpp | 473 | ||||
-rw-r--r-- | src/history/historymanager.h | 175 | ||||
-rw-r--r-- | src/history/historymodels.cpp | 743 | ||||
-rw-r--r-- | src/history/historymodels.h | 179 |
6 files changed, 1743 insertions, 0 deletions
diff --git a/src/history/autosaver.cpp b/src/history/autosaver.cpp new file mode 100644 index 00000000..ee84e299 --- /dev/null +++ b/src/history/autosaver.cpp @@ -0,0 +1,96 @@ +/* ============================================================ +* +* This file is a part of the rekonq project +* +* Copyright (C) 2007-2008 Trolltech ASA. All rights reserved +* Copyright (C) 2008-2011 by Andrea Diamantini <adjam7 at gmail dot com> +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License as +* published by the Free Software Foundation; either version 2 of +* the License or (at your option) version 3 or any later version +* accepted by the membership of KDE e.V. (or its successor approved +* by the membership of KDE e.V.), which shall act as a proxy +* defined in Section 14 of version 3 of the license. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* ============================================================ */ + + +// Self Includes +#include "autosaver.h" +#include "autosaver.moc" + +// Qt Includes +#include <QMetaObject> +#include <QTimerEvent> +#include <QBasicTimer> +#include <QTime> + + +const int AUTOSAVE_TIME = 1000 * 3; // seconds +const int MAX_TIME_LIMIT = 1000 * 15; // seconds + + +AutoSaver::AutoSaver(QObject *parent) + : QObject(parent) + , m_timer(new QBasicTimer) + , m_firstChange(new QTime) +{ +} + + +AutoSaver::~AutoSaver() +{ + if (m_timer->isActive()) + kDebug() << "AutoSaver: still active when destroyed, changes not saved."; + + delete m_firstChange; + delete m_timer; +} + + +void AutoSaver::saveIfNeccessary() +{ + if (m_timer->isActive()) + save(); +} + + +void AutoSaver::changeOccurred() +{ + if (m_firstChange->isNull()) + m_firstChange->start(); + + if (m_firstChange->elapsed() > MAX_TIME_LIMIT) + save(); + else + m_timer->start(AUTOSAVE_TIME, this); +} + + +void AutoSaver::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_timer->timerId()) + save(); + else + QObject::timerEvent(event); +} + + +void AutoSaver::save() +{ + m_timer->stop(); + delete m_firstChange; + m_firstChange = new QTime; + + emit saveNeeded(); +} diff --git a/src/history/autosaver.h b/src/history/autosaver.h new file mode 100644 index 00000000..cb1add2f --- /dev/null +++ b/src/history/autosaver.h @@ -0,0 +1,77 @@ +/* ============================================================ +* +* This file is a part of the rekonq project +* +* Copyright (C) 2007-2008 Trolltech ASA. All rights reserved +* Copyright (C) 2008-2011 by Andrea Diamantini <adjam7 at gmail dot com> +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License as +* published by the Free Software Foundation; either version 2 of +* the License or (at your option) version 3 or any later version +* accepted by the membership of KDE e.V. (or its successor approved +* by the membership of KDE e.V.), which shall act as a proxy +* defined in Section 14 of version 3 of the license. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* ============================================================ */ + + +#ifndef AUTOSAVER_H +#define AUTOSAVER_H + + +// Rekonq Includes +#include "rekonq_defines.h" + +// Qt Includes +#include <QObject> + +// Forward Declarations +class QBasicTimer; +class QTime; + + +/** + * This class emits the saveNeeded() signal. + * It will wait several seconds after changeOccurred() to combine + * multiple changes preventing continuous writing to disk. + */ +class AutoSaver : public QObject +{ + Q_OBJECT + +public: + explicit AutoSaver(QObject *parent); + virtual ~AutoSaver(); + + /** + * Emits the saveNeeded() signal if there's been any change since we last saved. + */ + void saveIfNeccessary(); + +Q_SIGNALS: + void saveNeeded(); + +public Q_SLOTS: + void changeOccurred(); + +protected: + virtual void timerEvent(QTimerEvent *event); + +private: + void save(); + + QBasicTimer *m_timer; + QTime *m_firstChange; +}; + +#endif // AUTOSAVER_H diff --git a/src/history/historymanager.cpp b/src/history/historymanager.cpp new file mode 100644 index 00000000..6fe13b73 --- /dev/null +++ b/src/history/historymanager.cpp @@ -0,0 +1,473 @@ +/* ============================================================ +* +* This file is a part of the rekonq project +* +* Copyright (C) 2007-2008 Trolltech ASA. All rights reserved +* Copyright (C) 2008 Benjamin C. Meyer <ben@meyerhome.net> +* Copyright (C) 2008-2012 by Andrea Diamantini <adjam7 at gmail dot com> +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License as +* published by the Free Software Foundation; either version 2 of +* the License or (at your option) version 3 or any later version +* accepted by the membership of KDE e.V. (or its successor approved +* by the membership of KDE e.V.), which shall act as a proxy +* defined in Section 14 of version 3 of the license. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* ============================================================ */ + + +// Self Includes +#include "historymanager.h" +#include "historymanager.moc" + +// Auto Includes +#include "rekonq.h" + +// Local Includes +#include "historymodels.h" +#include "autosaver.h" + +// KDE Includes +#include <KStandardDirs> +#include <KLocale> +#include <KCompletion> + +// Qt Includes +#include <QList> +#include <QUrl> +#include <QDate> +#include <QDateTime> +#include <QString> +#include <QFile> +#include <QDataStream> +#include <QBuffer> +#include <QTemporaryFile> +#include <QTimer> + +#include <QClipboard> + +// generic algorithms +#include <QtAlgorithms> + + +static const unsigned int HISTORY_VERSION = 25; + + +QWeakPointer<HistoryManager> HistoryManager::s_historyManager; + + +HistoryManager *HistoryManager::self() +{ + if (s_historyManager.isNull()) + { + s_historyManager = new HistoryManager(qApp); + } + return s_historyManager.data(); +} + + +// ---------------------------------------------------------------------------------------------- + + +HistoryManager::HistoryManager(QObject *parent) + : QObject(parent) + , m_saveTimer(new AutoSaver(this)) + , m_historyLimit(0) + , m_historyTreeModel(0) +{ + kDebug() << "loading history"; + connect(this, SIGNAL(entryAdded(HistoryItem)), m_saveTimer, SLOT(changeOccurred())); + connect(this, SIGNAL(entryRemoved(HistoryItem)), m_saveTimer, SLOT(changeOccurred())); + connect(m_saveTimer, SIGNAL(saveNeeded()), this, SLOT(save())); + + load(); + + HistoryModel *historyModel = new HistoryModel(this, this); + m_historyFilterModel = new HistoryFilterModel(historyModel, this); + m_historyTreeModel = new HistoryTreeModel(m_historyFilterModel, this); +} + + +HistoryManager::~HistoryManager() +{ + if (ReKonfig::expireHistory() == 4) + { + m_history.clear(); + save(); + return; + } + m_saveTimer->saveIfNeccessary(); + + kDebug() << "bye bye history..."; +} + + +bool HistoryManager::historyContains(const QString &url) const +{ + return m_historyFilterModel->historyContains(url); +} + + +void HistoryManager::addHistoryEntry(const KUrl &url, const QString &title) +{ + if (ReKonfig::expireHistory() == 5) // DON'T STORE HISTORY! + return; + + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) + return; + + if (url.isEmpty()) + return; + + QUrl urlToClean(url); + + // don't store about: urls (home page related) + if (urlToClean.scheme() == QString("about")) + return; + + urlToClean.setPassword(QString()); + urlToClean.setHost(urlToClean.host().toLower()); + QString urlString = urlToClean.toString(); + + HistoryItem item; + + // NOTE + // check if the url has just been visited. + // if so, remove previous entry from history, update and prepend it + if (historyContains(urlString)) + { + int index = m_historyFilterModel->historyLocation(urlString); + item = m_history.at(index); + m_history.removeOne(item); + emit entryRemoved(item); + + item.lastDateTimeVisit = QDateTime::currentDateTime(); + item.visitCount++; + } + else + { + item = HistoryItem(urlString, QDateTime::currentDateTime(), title); + } + + m_history.prepend(item); + emit entryAdded(item); + + if (m_history.count() == 1) + checkForExpired(); +} + + +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(); + + if (loadedAndSorted) + { + m_lastSavedUrl = m_history.value(0).url; + } + else + { + m_lastSavedUrl.clear(); + m_saveTimer->changeOccurred(); + } + + emit historyReset(); +} + + +void HistoryManager::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().lastDateTimeVisit; + 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(); + // remove from saved file also + m_lastSavedUrl.clear(); + emit entryRemoved(item); + } + + if (nextTimeout > 0) + QTimer::singleShot(nextTimeout * 1000, this, SLOT(checkForExpired())); +} + + +void HistoryManager::removeHistoryEntry(const KUrl &url, const QString &title) +{ + HistoryItem item; + for (int i = 0; i < m_history.count(); ++i) + { + if (url == m_history.at(i).url + && (title.isEmpty() || title == m_history.at(i).title)) + { + item = m_history.at(i); + m_lastSavedUrl.clear(); + m_history.removeOne(item); + emit entryRemoved(item); + break; + } + } +} + + +QList<HistoryItem> HistoryManager::find(const QString &text) +{ + QList<HistoryItem> list; + + QStringList urlKeys = m_historyFilterModel->keys(); + Q_FOREACH(const QString & url, urlKeys) + { + int index = m_historyFilterModel->historyLocation(url); + HistoryItem item = m_history.at(index); + + QStringList words = text.split(' '); + bool matches = true; + Q_FOREACH(const QString & word, words) + { + if (!url.contains(word, Qt::CaseInsensitive) + && !item.title.contains(word, Qt::CaseInsensitive)) + { + matches = false; + break; + } + } + if (matches) + list << item; + } + + return list; +} + + +void HistoryManager::clear() +{ + m_history.clear(); + m_lastSavedUrl.clear(); + m_saveTimer->changeOccurred(); + m_saveTimer->saveIfNeccessary(); + historyReset(); +} + + +void HistoryManager::loadSettings() +{ + int historyExpire = ReKonfig::expireHistory(); + int days; + switch (historyExpire) + { + case 1: + days = 90; + break; + case 2: + days = 30; + break; + case 3: + days = 1; + break; + case 0: + case 4: + case 5: + default: + days = -1; + break; + } + m_historyLimit = days; +} + + +void HistoryManager::load() +{ + loadSettings(); + + QString historyFilePath = KStandardDirs::locateLocal("appdata" , "history"); + QFile historyFile(historyFilePath); + if (!historyFile.exists()) + return; + if (!historyFile.open(QFile::ReadOnly)) + { + kDebug() << "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 version; + stream >> version; + + HistoryItem item; + + switch (version) + { + case HISTORY_VERSION: // default case + stream >> item.url; + stream >> item.firstDateTimeVisit; + stream >> item.lastDateTimeVisit; + stream >> item.title; + stream >> item.visitCount; + break; + + case 24: // this was history structure for rekonq < 0.8 + stream >> item.url; + stream >> item.lastDateTimeVisit; + stream >> item.title; + stream >> item.visitCount; + item.firstDateTimeVisit = item.lastDateTimeVisit; + break; + + case 23: // this will be used to upgrade previous structure... + stream >> item.url; + stream >> item.lastDateTimeVisit; + stream >> item.title; + item.visitCount = 1; + item.firstDateTimeVisit = item.lastDateTimeVisit; + break; + + default: + continue; + }; + + if (!item.lastDateTimeVisit.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.clear(); + m_saveTimer->changeOccurred(); + } +} + + +void HistoryManager::save() +{ + 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 historyFilePath = KStandardDirs::locateLocal("appdata" , "history"); + QFile historyFile(historyFilePath); + + // 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) + { + kDebug() << "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.firstDateTimeVisit << item.lastDateTimeVisit << item.title << item.visitCount; + out << data; + } + tempFile.close(); + + if (saveAll) + { + if (historyFile.exists() && !historyFile.remove()) + { + kDebug() << "History: error removing old history." << historyFile.errorString(); + } + if (!tempFile.rename(historyFile.fileName())) + { + kDebug() << "History: error moving new history over old." << tempFile.errorString() << historyFile.fileName(); + } + } + m_lastSavedUrl = m_history.value(0).url; + + emit historySaved(); +} diff --git a/src/history/historymanager.h b/src/history/historymanager.h new file mode 100644 index 00000000..96de8ab8 --- /dev/null +++ b/src/history/historymanager.h @@ -0,0 +1,175 @@ +/* ============================================================ +* +* This file is a part of the rekonq project +* +* Copyright (C) 2007-2008 Trolltech ASA. All rights reserved +* Copyright (C) 2008 Benjamin C. Meyer <ben@meyerhome.net> +* Copyright (C) 2008-2012 by Andrea Diamantini <adjam7 at gmail dot com> +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License as +* published by the Free Software Foundation; either version 2 of +* the License or (at your option) version 3 or any later version +* accepted by the membership of KDE e.V. (or its successor approved +* by the membership of KDE e.V.), which shall act as a proxy +* defined in Section 14 of version 3 of the license. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* ============================================================ */ + + +#ifndef HISTORY_H +#define HISTORY_H + + +// KDE Includes +#include <KUrl> + +// Qt Includes +#include <QDateTime> +#include <QHash> +#include <QObject> +#include <QWebHistory> + +#include <math.h> + +// Forward Declarations +class AutoSaver; +class HistoryFilterModel; +class HistoryTreeModel; + +class QWebHistory; + + +/** + * Elements in this class represent an history item + * + */ +class HistoryItem +{ +public: + HistoryItem() : visitCount(1) + {} + + explicit HistoryItem(const QString &u, + const QDateTime &d = QDateTime(), + const QString &t = QString() + ) + : title(t) + , url(u) + , firstDateTimeVisit(d) + , lastDateTimeVisit(d) + , visitCount(1) + {} + + inline bool operator==(const HistoryItem &other) const + { + return other.title == title + && other.url == url + && other.firstDateTimeVisit == firstDateTimeVisit + && other.lastDateTimeVisit == lastDateTimeVisit; + } + + inline qreal relevance() const + { + return log(visitCount) - log(lastDateTimeVisit.daysTo(QDateTime::currentDateTime()) + 1); + } + + // history is sorted in reverse + inline bool operator <(const HistoryItem &other) const + { + return lastDateTimeVisit > other.lastDateTimeVisit; + } + + QString title; + QString url; + QDateTime firstDateTimeVisit; + QDateTime lastDateTimeVisit; + int visitCount; +}; + + +// --------------------------------------------------------------------------------------------------------------- + + +/** + * THE History Manager: + * It manages rekonq history + * + */ +class HistoryManager : public QObject +{ + Q_OBJECT + +public: + /** + * Entry point. + * Access to HistoryManager class by using + * HistoryManager::self()->thePublicMethodYouNeed() + */ + static HistoryManager *self(); + + ~HistoryManager(); + + bool historyContains(const QString &url) const; + void addHistoryEntry(const KUrl &url, const QString &title); + void removeHistoryEntry(const KUrl &url, const QString &title = QString()); + + QList<HistoryItem> find(const QString &text); + + QList<HistoryItem> history() const + { + return m_history; + }; + void setHistory(const QList<HistoryItem> &history, bool loadedAndSorted = false); + + // History manager keeps around these models for use by the completer and other classes + HistoryFilterModel *historyFilterModel() const + { + return m_historyFilterModel; + }; + HistoryTreeModel *historyTreeModel() const + { + return m_historyTreeModel; + }; + +Q_SIGNALS: + void historyReset(); + void entryAdded(const HistoryItem &item); + void entryRemoved(const HistoryItem &item); + + void historySaved(); + +public Q_SLOTS: + void clear(); + void loadSettings(); + +private Q_SLOTS: + void save(); + void checkForExpired(); + +private: + HistoryManager(QObject *parent = 0); + void load(); + + AutoSaver *m_saveTimer; + int m_historyLimit; + QList<HistoryItem> m_history; + QString m_lastSavedUrl; + + HistoryFilterModel *m_historyFilterModel; + HistoryTreeModel *m_historyTreeModel; + + static QWeakPointer<HistoryManager> s_historyManager; +}; + + +#endif // HISTORY_H diff --git a/src/history/historymodels.cpp b/src/history/historymodels.cpp new file mode 100644 index 00000000..76c00a52 --- /dev/null +++ b/src/history/historymodels.cpp @@ -0,0 +1,743 @@ +/* ============================================================ +* +* This file is a part of the rekonq project +* +* Copyright (C) 2007-2008 Trolltech ASA. All rights reserved +* Copyright (C) 2008 Benjamin C. Meyer <ben@meyerhome.net> +* Copyright (C) 2008-2012 by Andrea Diamantini <adjam7 at gmail dot com> +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License as +* published by the Free Software Foundation; either version 2 of +* the License or (at your option) version 3 or any later version +* accepted by the membership of KDE e.V. (or its successor approved +* by the membership of KDE e.V.), which shall act as a proxy +* defined in Section 14 of version 3 of the license. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* ============================================================ */ + + +// Self Includes +#include "historymodels.h" +#include "historymodels.moc" + +// Auto Includes +#include "rekonq.h" + +// Local Includes +#include "historymanager.h" +// #include "iconmanager.h" + +// KDE Includes +#include <KStandardDirs> +#include <KLocale> +#include <KIcon> + +// Qt Includes +#include <QList> +#include <QUrl> +#include <QDate> +#include <QDateTime> +#include <QString> +#include <QFile> +#include <QDataStream> +#include <QBuffer> + +#include <QClipboard> +#include <QFileInfo> + +// generic algorithms +#include <QtAlgorithms> + + +HistoryModel::HistoryModel(HistoryManager *history, QObject *parent) + : QAbstractTableModel(parent) + , m_historyManager(history) +{ + Q_ASSERT(m_historyManager); + connect(m_historyManager, SIGNAL(historyReset()), this, SLOT(historyReset())); + connect(m_historyManager, SIGNAL(entryRemoved(HistoryItem)), this, SLOT(historyReset())); + connect(m_historyManager, SIGNAL(entryAdded(HistoryItem)), this, SLOT(entryAdded())); +} + + +void HistoryModel::historyReset() +{ + reset(); +} + + +void HistoryModel::entryAdded() +{ + beginInsertRows(QModelIndex(), 0, 0); + endInsertRows(); +} + + +QVariant HistoryModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal + && role == Qt::DisplayRole) + { + switch (section) + { + case 0: return i18n("Title"); + case 1: return i18n("Address"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + + +QVariant HistoryModel::data(const QModelIndex &index, int role) const +{ + QList<HistoryItem> lst = m_historyManager->history(); + if (index.row() < 0 || index.row() >= lst.size()) + return QVariant(); + + const HistoryItem &item = lst.at(index.row()); + switch (role) + { + case DateTimeRole: + return item.lastDateTimeVisit; + case DateRole: + return item.lastDateTimeVisit.date(); + case FirstDateTimeVisitRole: + return item.firstDateTimeVisit; + case UrlRole: + return QUrl(item.url); + case Qt::UserRole: + return KUrl(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 QIcon(); //FIXME rApp->iconManager()->iconForUrl(item.url); + } + case Qt::ToolTipRole: + QString tooltip = ""; + if (!item.title.isEmpty()) + tooltip = item.title + "<br/>"; + + QString lastVisit = item.firstDateTimeVisit.toString(Qt::SystemLocaleDate); + QString firstVisit = item.lastDateTimeVisit.toString(Qt::SystemLocaleDate); + int visitCount = item.visitCount; + tooltip += "<center> <b>" + item.url + "</b> </center>"; + tooltip += "<hr/>"; + tooltip += i18n("First Visit: ") + firstVisit + "<br/>"; + tooltip += i18n("Last Visit: ") + lastVisit + "<br/>"; + tooltip += i18n("Number of Visits: ") + QString::number(visitCount); + + return tooltip; + } + 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_historyManager->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_historyManager->history(); + for (int i = lastRow; i >= row; --i) + lst.removeAt(i); + disconnect(m_historyManager, SIGNAL(historyReset()), this, SLOT(historyReset())); + m_historyManager->setHistory(lst); + connect(m_historyManager, SIGNAL(historyReset()), this, SLOT(historyReset())); + endRemoveRows(); + return true; +} + + +// ------------------------------------------------------------------------------------------------------------------ + + +HistoryFilterModel::HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent) + : QAbstractProxyModel(parent) + , m_loaded(false) +{ + setSourceModel(sourceModel); +} + + +int HistoryFilterModel::historyLocation(const QString &url) const +{ + load(); + if (!m_historyHash.contains(url)) + return 0; + return sourceModel()->rowCount() - m_historyHash.value(url); +} + + +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(QModelIndex, QModelIndex)), + this, SLOT(dataChanged(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()) + { + m_loaded = false; + 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))); + } +} + + +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() +{ + m_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 (parent.isValid()) ? 0 : 2; +} + + +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.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 +{ + 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 (m_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; + } + } + m_loaded = true; +} + + +void HistoryFilterModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(end); + + if (start != 0) + { + kDebug() << "STARTING from a NON zero position..."; + return; + } + + if (!m_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 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()) + reset(); + return true; +} + + +// ---------------------------------------------------------------------------------------------------------- + + +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 i18n("Earlier Today"); + return date.toString(QL1S("dddd, MMMM d, yyyy")); + } + if (index.column() == 1) + { + return i18np("1 item", "%1 items", rowCount(index.sibling(index.row(), 0))); + } + } + } + + if (role == Qt::DecorationRole && index.column() == 0 && !index.parent().isValid()) + return KIcon("view-history"); + + 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); + } + + if (role == HistoryModel::FirstDateTimeVisitRole && index.column() == 0 && index.internalId() == 0) + { + int offset = sourceDateRow(index.row()); + QModelIndex idx = sourceModel()->index(offset, 0); + return idx.data(HistoryModel::FirstDateTimeVisitRole); + } + + 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, 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 | 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) +{ + 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))); + } + + 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))); + } + + reset(); +} + + +void HistoryTreeModel::sourceReset() +{ + m_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) + { + m_sourceRowCache.clear(); + reset(); + 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()) + { + m_sourceRowCache.clear(); + reset(); + 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(); + } +} diff --git a/src/history/historymodels.h b/src/history/historymodels.h new file mode 100644 index 00000000..44569fa2 --- /dev/null +++ b/src/history/historymodels.h @@ -0,0 +1,179 @@ +/* ============================================================ +* +* This file is a part of the rekonq project +* +* Copyright (C) 2007-2008 Trolltech ASA. All rights reserved +* Copyright (C) 2008 Benjamin C. Meyer <ben@meyerhome.net> +* Copyright (C) 2008-2012 by Andrea Diamantini <adjam7 at gmail dot com> +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License as +* published by the Free Software Foundation; either version 2 of +* the License or (at your option) version 3 or any later version +* accepted by the membership of KDE e.V. (or its successor approved +* by the membership of KDE e.V.), which shall act as a proxy +* defined in Section 14 of version 3 of the license. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* ============================================================ */ + + +#ifndef HISTORYMODELS_H +#define HISTORYMODELS_H + + +// Rekonq Includes +#include "rekonq_defines.h" + +// KDE Includes +#include <KUrl> + +// Qt Includes +#include <QHash> +#include <QAbstractTableModel> +#include <QAbstractProxyModel> + +// Forward Declarations +class HistoryManager; + + +class HistoryModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Roles + { + DateRole = Qt::UserRole + 1, + DateTimeRole = Qt::UserRole + 2, + UrlRole = Qt::UserRole + 3, + UrlStringRole = Qt::UserRole + 4, + FirstDateTimeVisitRole = Qt::UserRole + 5 + }; + + explicit HistoryModel(HistoryManager *history, QObject *parent = 0); + +public Q_SLOTS: + void historyReset(); + void entryAdded(); + +public: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + +private: + HistoryManager *m_historyManager; +}; + + +// ---------------------------------------------------------------------------------------------------- + + +/** + * Proxy model that will remove any duplicate entries. + * Both m_sourceRow and m_historyHash store their offsets not from + * the front of the list, but as offsets from the back. + * + */ +class HistoryFilterModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + explicit HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent = 0); + + inline bool historyContains(const QString &url) const + { + load(); + return m_historyHash.contains(url); + } + + inline QList<QString> keys() const + { + load(); + return m_historyHash.keys(); + } + + int historyLocation(const QString &url) const; + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const; + void setSourceModel(QAbstractItemModel *sourceModel); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex index(int, int, const QModelIndex& = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + +private Q_SLOTS: + void sourceReset(); + void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void sourceRowsInserted(const QModelIndex &parent, int start, int end); + void sourceRowsRemoved(const QModelIndex &, int, int); + +private: + void load() const; + + mutable QList<int> m_sourceRow; + mutable QHash<QString, int> m_historyHash; + mutable bool m_loaded; +}; + + +// ---------------------------------------------------------------------------------------------------------------------- + + +/** + * Proxy model for the history model that converts the list + * into a tree, one top level node per day. + * + * Used in the HistoryDialog. + * + */ + +class HistoryTreeModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + explicit HistoryTreeModel(QAbstractItemModel *sourceModel, QObject *parent = 0); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index = QModelIndex()) const; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + + void setSourceModel(QAbstractItemModel *sourceModel); + +private Q_SLOTS: + void sourceReset(); + void sourceRowsInserted(const QModelIndex &parent, int start, int end); + void sourceRowsRemoved(const QModelIndex &parent, int start, int end); + +private: + int sourceDateRow(int row) const; + mutable QList<int> m_sourceRowCache; +}; + + +#endif // HISTORYMODELS_H |