Merge branch 'master' of scm.dev.nokia.troll.no:qt/oslo-staging-1 into master-integration
[qt-netbsd.git] / demos / browser / history.cpp
blob386d65ca73d0f2cc7c5ed2506cee0c25dad7e3af
1 /****************************************************************************
2 **
3 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the demonstration applications of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** No Commercial Usage
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
14 ** this package.
16 ** GNU Lesser General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU Lesser
18 ** General Public License version 2.1 as published by the Free Software
19 ** Foundation and appearing in the file LICENSE.LGPL included in the
20 ** packaging of this file. Please review the following information to
21 ** ensure the GNU Lesser General Public License version 2.1 requirements
22 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 ** In addition, as a special exception, Nokia gives you certain additional
25 ** rights. These rights are described in the Nokia Qt LGPL Exception
26 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
38 ** $QT_END_LICENSE$
40 ****************************************************************************/
42 #include "history.h"
44 #include "autosaver.h"
45 #include "browserapplication.h"
47 #include <QtCore/QBuffer>
48 #include <QtCore/QDir>
49 #include <QtCore/QFile>
50 #include <QtCore/QFileInfo>
51 #include <QtCore/QSettings>
52 #include <QtCore/QTemporaryFile>
53 #include <QtCore/QTextStream>
55 #include <QtCore/QtAlgorithms>
57 #include <QtGui/QClipboard>
58 #include <QtGui/QDesktopServices>
59 #include <QtGui/QHeaderView>
60 #include <QtGui/QStyle>
62 #include <QtWebKit/QWebHistoryInterface>
63 #include <QtWebKit/QWebSettings>
65 #include <QtCore/QDebug>
67 static const unsigned int HISTORY_VERSION = 23;
69 HistoryManager::HistoryManager(QObject *parent)
70 : QWebHistoryInterface(parent)
71 , m_saveTimer(new AutoSaver(this))
72 , m_historyLimit(30)
73 , m_historyModel(0)
74 , m_historyFilterModel(0)
75 , m_historyTreeModel(0)
77 m_expiredTimer.setSingleShot(true);
78 connect(&m_expiredTimer, SIGNAL(timeout()),
79 this, SLOT(checkForExpired()));
80 connect(this, SIGNAL(entryAdded(HistoryItem)),
81 m_saveTimer, SLOT(changeOccurred()));
82 connect(this, SIGNAL(entryRemoved(HistoryItem)),
83 m_saveTimer, SLOT(changeOccurred()));
84 load();
86 m_historyModel = new HistoryModel(this, this);
87 m_historyFilterModel = new HistoryFilterModel(m_historyModel, this);
88 m_historyTreeModel = new HistoryTreeModel(m_historyFilterModel, this);
90 // QWebHistoryInterface will delete the history manager
91 QWebHistoryInterface::setDefaultInterface(this);
94 HistoryManager::~HistoryManager()
96 m_saveTimer->saveIfNeccessary();
99 QList<HistoryItem> HistoryManager::history() const
101 return m_history;
104 bool HistoryManager::historyContains(const QString &url) const
106 return m_historyFilterModel->historyContains(url);
109 void HistoryManager::addHistoryEntry(const QString &url)
111 QUrl cleanUrl(url);
112 cleanUrl.setPassword(QString());
113 cleanUrl.setHost(cleanUrl.host().toLower());
114 HistoryItem item(cleanUrl.toString(), QDateTime::currentDateTime());
115 addHistoryItem(item);
118 void HistoryManager::setHistory(const QList<HistoryItem> &history, bool loadedAndSorted)
120 m_history = history;
122 // verify that it is sorted by date
123 if (!loadedAndSorted)
124 qSort(m_history.begin(), m_history.end());
126 checkForExpired();
128 if (loadedAndSorted) {
129 m_lastSavedUrl = m_history.value(0).url;
130 } else {
131 m_lastSavedUrl = QString();
132 m_saveTimer->changeOccurred();
134 emit historyReset();
137 HistoryModel *HistoryManager::historyModel() const
139 return m_historyModel;
142 HistoryFilterModel *HistoryManager::historyFilterModel() const
144 return m_historyFilterModel;
147 HistoryTreeModel *HistoryManager::historyTreeModel() const
149 return m_historyTreeModel;
152 void HistoryManager::checkForExpired()
154 if (m_historyLimit < 0 || m_history.isEmpty())
155 return;
157 QDateTime now = QDateTime::currentDateTime();
158 int nextTimeout = 0;
160 while (!m_history.isEmpty()) {
161 QDateTime checkForExpired = m_history.last().dateTime;
162 checkForExpired.setDate(checkForExpired.date().addDays(m_historyLimit));
163 if (now.daysTo(checkForExpired) > 7) {
164 // check at most in a week to prevent int overflows on the timer
165 nextTimeout = 7 * 86400;
166 } else {
167 nextTimeout = now.secsTo(checkForExpired);
169 if (nextTimeout > 0)
170 break;
171 HistoryItem item = m_history.takeLast();
172 // remove from saved file also
173 m_lastSavedUrl = QString();
174 emit entryRemoved(item);
177 if (nextTimeout > 0)
178 m_expiredTimer.start(nextTimeout * 1000);
181 void HistoryManager::addHistoryItem(const HistoryItem &item)
183 QWebSettings *globalSettings = QWebSettings::globalSettings();
184 if (globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled))
185 return;
187 m_history.prepend(item);
188 emit entryAdded(item);
189 if (m_history.count() == 1)
190 checkForExpired();
193 void HistoryManager::updateHistoryItem(const QUrl &url, const QString &title)
195 for (int i = 0; i < m_history.count(); ++i) {
196 if (url == m_history.at(i).url) {
197 m_history[i].title = title;
198 m_saveTimer->changeOccurred();
199 if (m_lastSavedUrl.isEmpty())
200 m_lastSavedUrl = m_history.at(i).url;
201 emit entryUpdated(i);
202 break;
207 int HistoryManager::historyLimit() const
209 return m_historyLimit;
212 void HistoryManager::setHistoryLimit(int limit)
214 if (m_historyLimit == limit)
215 return;
216 m_historyLimit = limit;
217 checkForExpired();
218 m_saveTimer->changeOccurred();
221 void HistoryManager::clear()
223 m_history.clear();
224 m_lastSavedUrl = QString();
225 m_saveTimer->changeOccurred();
226 m_saveTimer->saveIfNeccessary();
227 historyReset();
230 void HistoryManager::loadSettings()
232 // load settings
233 QSettings settings;
234 settings.beginGroup(QLatin1String("history"));
235 m_historyLimit = settings.value(QLatin1String("historyLimit"), 30).toInt();
238 void HistoryManager::load()
240 loadSettings();
242 QFile historyFile(QDesktopServices::storageLocation(QDesktopServices::DataLocation)
243 + QLatin1String("/history"));
244 if (!historyFile.exists())
245 return;
246 if (!historyFile.open(QFile::ReadOnly)) {
247 qWarning() << "Unable to open history file" << historyFile.fileName();
248 return;
251 QList<HistoryItem> list;
252 QDataStream in(&historyFile);
253 // Double check that the history file is sorted as it is read in
254 bool needToSort = false;
255 HistoryItem lastInsertedItem;
256 QByteArray data;
257 QDataStream stream;
258 QBuffer buffer;
259 stream.setDevice(&buffer);
260 while (!historyFile.atEnd()) {
261 in >> data;
262 buffer.close();
263 buffer.setBuffer(&data);
264 buffer.open(QIODevice::ReadOnly);
265 quint32 ver;
266 stream >> ver;
267 if (ver != HISTORY_VERSION)
268 continue;
269 HistoryItem item;
270 stream >> item.url;
271 stream >> item.dateTime;
272 stream >> item.title;
274 if (!item.dateTime.isValid())
275 continue;
277 if (item == lastInsertedItem) {
278 if (lastInsertedItem.title.isEmpty() && !list.isEmpty())
279 list[0].title = item.title;
280 continue;
283 if (!needToSort && !list.isEmpty() && lastInsertedItem < item)
284 needToSort = true;
286 list.prepend(item);
287 lastInsertedItem = item;
289 if (needToSort)
290 qSort(list.begin(), list.end());
292 setHistory(list, true);
294 // If we had to sort re-write the whole history sorted
295 if (needToSort) {
296 m_lastSavedUrl = QString();
297 m_saveTimer->changeOccurred();
301 void HistoryManager::save()
303 QSettings settings;
304 settings.beginGroup(QLatin1String("history"));
305 settings.setValue(QLatin1String("historyLimit"), m_historyLimit);
307 bool saveAll = m_lastSavedUrl.isEmpty();
308 int first = m_history.count() - 1;
309 if (!saveAll) {
310 // find the first one to save
311 for (int i = 0; i < m_history.count(); ++i) {
312 if (m_history.at(i).url == m_lastSavedUrl) {
313 first = i - 1;
314 break;
318 if (first == m_history.count() - 1)
319 saveAll = true;
321 QString directory = QDesktopServices::storageLocation(QDesktopServices::DataLocation);
322 if (directory.isEmpty())
323 directory = QDir::homePath() + QLatin1String("/.") + QCoreApplication::applicationName();
324 if (!QFile::exists(directory)) {
325 QDir dir;
326 dir.mkpath(directory);
329 QFile historyFile(directory + QLatin1String("/history"));
330 // When saving everything use a temporary file to prevent possible data loss.
331 QTemporaryFile tempFile;
332 tempFile.setAutoRemove(false);
333 bool open = false;
334 if (saveAll) {
335 open = tempFile.open();
336 } else {
337 open = historyFile.open(QFile::Append);
340 if (!open) {
341 qWarning() << "Unable to open history file for saving"
342 << (saveAll ? tempFile.fileName() : historyFile.fileName());
343 return;
346 QDataStream out(saveAll ? &tempFile : &historyFile);
347 for (int i = first; i >= 0; --i) {
348 QByteArray data;
349 QDataStream stream(&data, QIODevice::WriteOnly);
350 HistoryItem item = m_history.at(i);
351 stream << HISTORY_VERSION << item.url << item.dateTime << item.title;
352 out << data;
354 tempFile.close();
356 if (saveAll) {
357 if (historyFile.exists() && !historyFile.remove())
358 qWarning() << "History: error removing old history." << historyFile.errorString();
359 if (!tempFile.rename(historyFile.fileName()))
360 qWarning() << "History: error moving new history over old." << tempFile.errorString() << historyFile.fileName();
362 m_lastSavedUrl = m_history.value(0).url;
365 HistoryModel::HistoryModel(HistoryManager *history, QObject *parent)
366 : QAbstractTableModel(parent)
367 , m_history(history)
369 Q_ASSERT(m_history);
370 connect(m_history, SIGNAL(historyReset()),
371 this, SLOT(historyReset()));
372 connect(m_history, SIGNAL(entryRemoved(HistoryItem)),
373 this, SLOT(historyReset()));
375 connect(m_history, SIGNAL(entryAdded(HistoryItem)),
376 this, SLOT(entryAdded()));
377 connect(m_history, SIGNAL(entryUpdated(int)),
378 this, SLOT(entryUpdated(int)));
381 void HistoryModel::historyReset()
383 reset();
386 void HistoryModel::entryAdded()
388 beginInsertRows(QModelIndex(), 0, 0);
389 endInsertRows();
392 void HistoryModel::entryUpdated(int offset)
394 QModelIndex idx = index(offset, 0);
395 emit dataChanged(idx, idx);
398 QVariant HistoryModel::headerData(int section, Qt::Orientation orientation, int role) const
400 if (orientation == Qt::Horizontal
401 && role == Qt::DisplayRole) {
402 switch (section) {
403 case 0: return tr("Title");
404 case 1: return tr("Address");
407 return QAbstractTableModel::headerData(section, orientation, role);
410 QVariant HistoryModel::data(const QModelIndex &index, int role) const
412 QList<HistoryItem> lst = m_history->history();
413 if (index.row() < 0 || index.row() >= lst.size())
414 return QVariant();
416 const HistoryItem &item = lst.at(index.row());
417 switch (role) {
418 case DateTimeRole:
419 return item.dateTime;
420 case DateRole:
421 return item.dateTime.date();
422 case UrlRole:
423 return QUrl(item.url);
424 case UrlStringRole:
425 return item.url;
426 case Qt::DisplayRole:
427 case Qt::EditRole: {
428 switch (index.column()) {
429 case 0:
430 // when there is no title try to generate one from the url
431 if (item.title.isEmpty()) {
432 QString page = QFileInfo(QUrl(item.url).path()).fileName();
433 if (!page.isEmpty())
434 return page;
435 return item.url;
437 return item.title;
438 case 1:
439 return item.url;
442 case Qt::DecorationRole:
443 if (index.column() == 0) {
444 return BrowserApplication::instance()->icon(item.url);
447 return QVariant();
450 int HistoryModel::columnCount(const QModelIndex &parent) const
452 return (parent.isValid()) ? 0 : 2;
455 int HistoryModel::rowCount(const QModelIndex &parent) const
457 return (parent.isValid()) ? 0 : m_history->history().count();
460 bool HistoryModel::removeRows(int row, int count, const QModelIndex &parent)
462 if (parent.isValid())
463 return false;
464 int lastRow = row + count - 1;
465 beginRemoveRows(parent, row, lastRow);
466 QList<HistoryItem> lst = m_history->history();
467 for (int i = lastRow; i >= row; --i)
468 lst.removeAt(i);
469 disconnect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset()));
470 m_history->setHistory(lst);
471 connect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset()));
472 endRemoveRows();
473 return true;
476 #define MOVEDROWS 15
479 Maps the first bunch of items of the source model to the root
481 HistoryMenuModel::HistoryMenuModel(HistoryTreeModel *sourceModel, QObject *parent)
482 : QAbstractProxyModel(parent)
483 , m_treeModel(sourceModel)
485 setSourceModel(sourceModel);
488 int HistoryMenuModel::bumpedRows() const
490 QModelIndex first = m_treeModel->index(0, 0);
491 if (!first.isValid())
492 return 0;
493 return qMin(m_treeModel->rowCount(first), MOVEDROWS);
496 int HistoryMenuModel::columnCount(const QModelIndex &parent) const
498 return m_treeModel->columnCount(mapToSource(parent));
501 int HistoryMenuModel::rowCount(const QModelIndex &parent) const
503 if (parent.column() > 0)
504 return 0;
506 if (!parent.isValid()) {
507 int folders = sourceModel()->rowCount();
508 int bumpedItems = bumpedRows();
509 if (bumpedItems <= MOVEDROWS
510 && bumpedItems == sourceModel()->rowCount(sourceModel()->index(0, 0)))
511 --folders;
512 return bumpedItems + folders;
515 if (parent.internalId() == -1) {
516 if (parent.row() < bumpedRows())
517 return 0;
520 QModelIndex idx = mapToSource(parent);
521 int defaultCount = sourceModel()->rowCount(idx);
522 if (idx == sourceModel()->index(0, 0))
523 return defaultCount - bumpedRows();
524 return defaultCount;
527 QModelIndex HistoryMenuModel::mapFromSource(const QModelIndex &sourceIndex) const
529 // currently not used or autotested
530 Q_ASSERT(false);
531 int sr = m_treeModel->mapToSource(sourceIndex).row();
532 return createIndex(sourceIndex.row(), sourceIndex.column(), sr);
535 QModelIndex HistoryMenuModel::mapToSource(const QModelIndex &proxyIndex) const
537 if (!proxyIndex.isValid())
538 return QModelIndex();
540 if (proxyIndex.internalId() == -1) {
541 int bumpedItems = bumpedRows();
542 if (proxyIndex.row() < bumpedItems)
543 return m_treeModel->index(proxyIndex.row(), proxyIndex.column(), m_treeModel->index(0, 0));
544 if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(m_treeModel->index(0, 0)))
545 --bumpedItems;
546 return m_treeModel->index(proxyIndex.row() - bumpedItems, proxyIndex.column());
549 QModelIndex historyIndex = m_treeModel->sourceModel()->index(proxyIndex.internalId(), proxyIndex.column());
550 QModelIndex treeIndex = m_treeModel->mapFromSource(historyIndex);
551 return treeIndex;
554 QModelIndex HistoryMenuModel::index(int row, int column, const QModelIndex &parent) const
556 if (row < 0
557 || column < 0 || column >= columnCount(parent)
558 || parent.column() > 0)
559 return QModelIndex();
560 if (!parent.isValid())
561 return createIndex(row, column, -1);
563 QModelIndex treeIndexParent = mapToSource(parent);
565 int bumpedItems = 0;
566 if (treeIndexParent == m_treeModel->index(0, 0))
567 bumpedItems = bumpedRows();
568 QModelIndex treeIndex = m_treeModel->index(row + bumpedItems, column, treeIndexParent);
569 QModelIndex historyIndex = m_treeModel->mapToSource(treeIndex);
570 int historyRow = historyIndex.row();
571 if (historyRow == -1)
572 historyRow = treeIndex.row();
573 return createIndex(row, column, historyRow);
576 QModelIndex HistoryMenuModel::parent(const QModelIndex &index) const
578 int offset = index.internalId();
579 if (offset == -1 || !index.isValid())
580 return QModelIndex();
582 QModelIndex historyIndex = m_treeModel->sourceModel()->index(index.internalId(), 0);
583 QModelIndex treeIndex = m_treeModel->mapFromSource(historyIndex);
584 QModelIndex treeIndexParent = treeIndex.parent();
586 int sr = m_treeModel->mapToSource(treeIndexParent).row();
587 int bumpedItems = bumpedRows();
588 if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(sourceModel()->index(0, 0)))
589 --bumpedItems;
590 return createIndex(bumpedItems + treeIndexParent.row(), treeIndexParent.column(), sr);
594 HistoryMenu::HistoryMenu(QWidget *parent)
595 : ModelMenu(parent)
596 , m_history(0)
598 connect(this, SIGNAL(activated(QModelIndex)),
599 this, SLOT(activated(QModelIndex)));
600 setHoverRole(HistoryModel::UrlStringRole);
603 void HistoryMenu::activated(const QModelIndex &index)
605 emit openUrl(index.data(HistoryModel::UrlRole).toUrl());
608 bool HistoryMenu::prePopulated()
610 if (!m_history) {
611 m_history = BrowserApplication::historyManager();
612 m_historyMenuModel = new HistoryMenuModel(m_history->historyTreeModel(), this);
613 setModel(m_historyMenuModel);
615 // initial actions
616 for (int i = 0; i < m_initialActions.count(); ++i)
617 addAction(m_initialActions.at(i));
618 if (!m_initialActions.isEmpty())
619 addSeparator();
620 setFirstSeparator(m_historyMenuModel->bumpedRows());
622 return false;
625 void HistoryMenu::postPopulated()
627 if (m_history->history().count() > 0)
628 addSeparator();
630 QAction *showAllAction = new QAction(tr("Show All History"), this);
631 connect(showAllAction, SIGNAL(triggered()), this, SLOT(showHistoryDialog()));
632 addAction(showAllAction);
634 QAction *clearAction = new QAction(tr("Clear History"), this);
635 connect(clearAction, SIGNAL(triggered()), m_history, SLOT(clear()));
636 addAction(clearAction);
639 void HistoryMenu::showHistoryDialog()
641 HistoryDialog *dialog = new HistoryDialog(this);
642 connect(dialog, SIGNAL(openUrl(QUrl)),
643 this, SIGNAL(openUrl(QUrl)));
644 dialog->show();
647 void HistoryMenu::setInitialActions(QList<QAction*> actions)
649 m_initialActions = actions;
650 for (int i = 0; i < m_initialActions.count(); ++i)
651 addAction(m_initialActions.at(i));
654 TreeProxyModel::TreeProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
656 setSortRole(HistoryModel::DateTimeRole);
657 setFilterCaseSensitivity(Qt::CaseInsensitive);
660 bool TreeProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
662 if (!source_parent.isValid())
663 return true;
664 return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
667 HistoryDialog::HistoryDialog(QWidget *parent, HistoryManager *setHistory) : QDialog(parent)
669 HistoryManager *history = setHistory;
670 if (!history)
671 history = BrowserApplication::historyManager();
672 setupUi(this);
673 tree->setUniformRowHeights(true);
674 tree->setSelectionBehavior(QAbstractItemView::SelectRows);
675 tree->setTextElideMode(Qt::ElideMiddle);
676 QAbstractItemModel *model = history->historyTreeModel();
677 TreeProxyModel *proxyModel = new TreeProxyModel(this);
678 connect(search, SIGNAL(textChanged(QString)),
679 proxyModel, SLOT(setFilterFixedString(QString)));
680 connect(removeButton, SIGNAL(clicked()), tree, SLOT(removeOne()));
681 connect(removeAllButton, SIGNAL(clicked()), history, SLOT(clear()));
682 proxyModel->setSourceModel(model);
683 tree->setModel(proxyModel);
684 tree->setExpanded(proxyModel->index(0, 0), true);
685 tree->setAlternatingRowColors(true);
686 QFontMetrics fm(font());
687 int header = fm.width(QLatin1Char('m')) * 40;
688 tree->header()->resizeSection(0, header);
689 tree->header()->setStretchLastSection(true);
690 connect(tree, SIGNAL(activated(QModelIndex)),
691 this, SLOT(open()));
692 tree->setContextMenuPolicy(Qt::CustomContextMenu);
693 connect(tree, SIGNAL(customContextMenuRequested(QPoint)),
694 this, SLOT(customContextMenuRequested(QPoint)));
697 void HistoryDialog::customContextMenuRequested(const QPoint &pos)
699 QMenu menu;
700 QModelIndex index = tree->indexAt(pos);
701 index = index.sibling(index.row(), 0);
702 if (index.isValid() && !tree->model()->hasChildren(index)) {
703 menu.addAction(tr("Open"), this, SLOT(open()));
704 menu.addSeparator();
705 menu.addAction(tr("Copy"), this, SLOT(copy()));
707 menu.addAction(tr("Delete"), tree, SLOT(removeOne()));
708 menu.exec(QCursor::pos());
711 void HistoryDialog::open()
713 QModelIndex index = tree->currentIndex();
714 if (!index.parent().isValid())
715 return;
716 emit openUrl(index.data(HistoryModel::UrlRole).toUrl());
719 void HistoryDialog::copy()
721 QModelIndex index = tree->currentIndex();
722 if (!index.parent().isValid())
723 return;
724 QString url = index.data(HistoryModel::UrlStringRole).toString();
726 QClipboard *clipboard = QApplication::clipboard();
727 clipboard->setText(url);
730 HistoryFilterModel::HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent)
731 : QAbstractProxyModel(parent),
732 m_loaded(false)
734 setSourceModel(sourceModel);
737 int HistoryFilterModel::historyLocation(const QString &url) const
739 load();
740 if (!m_historyHash.contains(url))
741 return 0;
742 return sourceModel()->rowCount() - m_historyHash.value(url);
745 QVariant HistoryFilterModel::data(const QModelIndex &index, int role) const
747 return QAbstractProxyModel::data(index, role);
750 void HistoryFilterModel::setSourceModel(QAbstractItemModel *newSourceModel)
752 if (sourceModel()) {
753 disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
754 disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
755 this, SLOT(dataChanged(QModelIndex,QModelIndex)));
756 disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
757 this, SLOT(sourceRowsInserted(QModelIndex,int,int)));
758 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
759 this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));
762 QAbstractProxyModel::setSourceModel(newSourceModel);
764 if (sourceModel()) {
765 m_loaded = false;
766 connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
767 connect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
768 this, SLOT(sourceDataChanged(QModelIndex,QModelIndex)));
769 connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
770 this, SLOT(sourceRowsInserted(QModelIndex,int,int)));
771 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
772 this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));
776 void HistoryFilterModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
778 emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight));
781 QVariant HistoryFilterModel::headerData(int section, Qt::Orientation orientation, int role) const
783 return sourceModel()->headerData(section, orientation, role);
786 void HistoryFilterModel::sourceReset()
788 m_loaded = false;
789 reset();
792 int HistoryFilterModel::rowCount(const QModelIndex &parent) const
794 load();
795 if (parent.isValid())
796 return 0;
797 return m_historyHash.count();
800 int HistoryFilterModel::columnCount(const QModelIndex &parent) const
802 return (parent.isValid()) ? 0 : 2;
805 QModelIndex HistoryFilterModel::mapToSource(const QModelIndex &proxyIndex) const
807 load();
808 int sourceRow = sourceModel()->rowCount() - proxyIndex.internalId();
809 return sourceModel()->index(sourceRow, proxyIndex.column());
812 QModelIndex HistoryFilterModel::mapFromSource(const QModelIndex &sourceIndex) const
814 load();
815 QString url = sourceIndex.data(HistoryModel::UrlStringRole).toString();
816 if (!m_historyHash.contains(url))
817 return QModelIndex();
819 // This can be done in a binary search, but we can't use qBinary find
820 // because it can't take: qBinaryFind(m_sourceRow.end(), m_sourceRow.begin(), v);
821 // so if this is a performance bottlneck then convert to binary search, until then
822 // the cleaner/easier to read code wins the day.
823 int realRow = -1;
824 int sourceModelRow = sourceModel()->rowCount() - sourceIndex.row();
826 for (int i = 0; i < m_sourceRow.count(); ++i) {
827 if (m_sourceRow.at(i) == sourceModelRow) {
828 realRow = i;
829 break;
832 if (realRow == -1)
833 return QModelIndex();
835 return createIndex(realRow, sourceIndex.column(), sourceModel()->rowCount() - sourceIndex.row());
838 QModelIndex HistoryFilterModel::index(int row, int column, const QModelIndex &parent) const
840 load();
841 if (row < 0 || row >= rowCount(parent)
842 || column < 0 || column >= columnCount(parent))
843 return QModelIndex();
845 return createIndex(row, column, m_sourceRow[row]);
848 QModelIndex HistoryFilterModel::parent(const QModelIndex &) const
850 return QModelIndex();
853 void HistoryFilterModel::load() const
855 if (m_loaded)
856 return;
857 m_sourceRow.clear();
858 m_historyHash.clear();
859 m_historyHash.reserve(sourceModel()->rowCount());
860 for (int i = 0; i < sourceModel()->rowCount(); ++i) {
861 QModelIndex idx = sourceModel()->index(i, 0);
862 QString url = idx.data(HistoryModel::UrlStringRole).toString();
863 if (!m_historyHash.contains(url)) {
864 m_sourceRow.append(sourceModel()->rowCount() - i);
865 m_historyHash[url] = sourceModel()->rowCount() - i;
868 m_loaded = true;
871 void HistoryFilterModel::sourceRowsInserted(const QModelIndex &parent, int start, int end)
873 Q_ASSERT(start == end && start == 0);
874 Q_UNUSED(end);
875 if (!m_loaded)
876 return;
877 QModelIndex idx = sourceModel()->index(start, 0, parent);
878 QString url = idx.data(HistoryModel::UrlStringRole).toString();
879 if (m_historyHash.contains(url)) {
880 int sourceRow = sourceModel()->rowCount() - m_historyHash[url];
881 int realRow = mapFromSource(sourceModel()->index(sourceRow, 0)).row();
882 beginRemoveRows(QModelIndex(), realRow, realRow);
883 m_sourceRow.removeAt(realRow);
884 m_historyHash.remove(url);
885 endRemoveRows();
887 beginInsertRows(QModelIndex(), 0, 0);
888 m_historyHash.insert(url, sourceModel()->rowCount() - start);
889 m_sourceRow.insert(0, sourceModel()->rowCount());
890 endInsertRows();
893 void HistoryFilterModel::sourceRowsRemoved(const QModelIndex &, int start, int end)
895 Q_UNUSED(start);
896 Q_UNUSED(end);
897 sourceReset();
901 Removing a continuous block of rows will remove filtered rows too as this is
902 the users intention.
904 bool HistoryFilterModel::removeRows(int row, int count, const QModelIndex &parent)
906 if (row < 0 || count <= 0 || row + count > rowCount(parent) || parent.isValid())
907 return false;
908 int lastRow = row + count - 1;
909 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
910 this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));
911 beginRemoveRows(parent, row, lastRow);
912 int oldCount = rowCount();
913 int start = sourceModel()->rowCount() - m_sourceRow.value(row);
914 int end = sourceModel()->rowCount() - m_sourceRow.value(lastRow);
915 sourceModel()->removeRows(start, end - start + 1);
916 endRemoveRows();
917 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
918 this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));
919 m_loaded = false;
920 if (oldCount - count != rowCount())
921 reset();
922 return true;
925 HistoryCompletionModel::HistoryCompletionModel(QObject *parent)
926 : QAbstractProxyModel(parent)
930 QVariant HistoryCompletionModel::data(const QModelIndex &index, int role) const
932 if (sourceModel()
933 && (role == Qt::EditRole || role == Qt::DisplayRole)
934 && index.isValid()) {
935 QModelIndex idx = mapToSource(index);
936 idx = idx.sibling(idx.row(), 1);
937 QString urlString = idx.data(HistoryModel::UrlStringRole).toString();
938 if (index.row() % 2) {
939 QUrl url = urlString;
940 QString s = url.toString(QUrl::RemoveScheme
941 | QUrl::RemoveUserInfo
942 | QUrl::StripTrailingSlash);
943 return s.mid(2); // strip // from the front
945 return urlString;
947 return QAbstractProxyModel::data(index, role);
950 int HistoryCompletionModel::rowCount(const QModelIndex &parent) const
952 return (parent.isValid() || !sourceModel()) ? 0 : sourceModel()->rowCount(parent) * 2;
955 int HistoryCompletionModel::columnCount(const QModelIndex &parent) const
957 return (parent.isValid()) ? 0 : 1;
960 QModelIndex HistoryCompletionModel::mapFromSource(const QModelIndex &sourceIndex) const
962 int row = sourceIndex.row() * 2;
963 return index(row, sourceIndex.column());
966 QModelIndex HistoryCompletionModel::mapToSource(const QModelIndex &proxyIndex) const
968 if (!sourceModel())
969 return QModelIndex();
970 int row = proxyIndex.row() / 2;
971 return sourceModel()->index(row, proxyIndex.column());
974 QModelIndex HistoryCompletionModel::index(int row, int column, const QModelIndex &parent) const
976 if (row < 0 || row >= rowCount(parent)
977 || column < 0 || column >= columnCount(parent))
978 return QModelIndex();
979 return createIndex(row, column, 0);
982 QModelIndex HistoryCompletionModel::parent(const QModelIndex &) const
984 return QModelIndex();
987 void HistoryCompletionModel::setSourceModel(QAbstractItemModel *newSourceModel)
989 if (sourceModel()) {
990 disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
991 disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
992 this, SLOT(sourceReset()));
993 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
994 this, SLOT(sourceReset()));
997 QAbstractProxyModel::setSourceModel(newSourceModel);
999 if (newSourceModel) {
1000 connect(newSourceModel, SIGNAL(modelReset()), this, SLOT(sourceReset()));
1001 connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
1002 this, SLOT(sourceReset()));
1003 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
1004 this, SLOT(sourceReset()));
1007 reset();
1010 void HistoryCompletionModel::sourceReset()
1012 reset();
1015 HistoryTreeModel::HistoryTreeModel(QAbstractItemModel *sourceModel, QObject *parent)
1016 : QAbstractProxyModel(parent)
1018 setSourceModel(sourceModel);
1021 QVariant HistoryTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
1023 return sourceModel()->headerData(section, orientation, role);
1026 QVariant HistoryTreeModel::data(const QModelIndex &index, int role) const
1028 if ((role == Qt::EditRole || role == Qt::DisplayRole)) {
1029 int start = index.internalId();
1030 if (start == 0) {
1031 int offset = sourceDateRow(index.row());
1032 if (index.column() == 0) {
1033 QModelIndex idx = sourceModel()->index(offset, 0);
1034 QDate date = idx.data(HistoryModel::DateRole).toDate();
1035 if (date == QDate::currentDate())
1036 return tr("Earlier Today");
1037 return date.toString(QLatin1String("dddd, MMMM d, yyyy"));
1039 if (index.column() == 1) {
1040 return tr("%1 items").arg(rowCount(index.sibling(index.row(), 0)));
1044 if (role == Qt::DecorationRole && index.column() == 0 && !index.parent().isValid())
1045 return QIcon(QLatin1String(":history.png"));
1046 if (role == HistoryModel::DateRole && index.column() == 0 && index.internalId() == 0) {
1047 int offset = sourceDateRow(index.row());
1048 QModelIndex idx = sourceModel()->index(offset, 0);
1049 return idx.data(HistoryModel::DateRole);
1052 return QAbstractProxyModel::data(index, role);
1055 int HistoryTreeModel::columnCount(const QModelIndex &parent) const
1057 return sourceModel()->columnCount(mapToSource(parent));
1060 int HistoryTreeModel::rowCount(const QModelIndex &parent) const
1062 if ( parent.internalId() != 0
1063 || parent.column() > 0
1064 || !sourceModel())
1065 return 0;
1067 // row count OF dates
1068 if (!parent.isValid()) {
1069 if (!m_sourceRowCache.isEmpty())
1070 return m_sourceRowCache.count();
1071 QDate currentDate;
1072 int rows = 0;
1073 int totalRows = sourceModel()->rowCount();
1075 for (int i = 0; i < totalRows; ++i) {
1076 QDate rowDate = sourceModel()->index(i, 0).data(HistoryModel::DateRole).toDate();
1077 if (rowDate != currentDate) {
1078 m_sourceRowCache.append(i);
1079 currentDate = rowDate;
1080 ++rows;
1083 Q_ASSERT(m_sourceRowCache.count() == rows);
1084 return rows;
1087 // row count FOR a date
1088 int start = sourceDateRow(parent.row());
1089 int end = sourceDateRow(parent.row() + 1);
1090 return (end - start);
1093 // Translate the top level date row into the offset where that date starts
1094 int HistoryTreeModel::sourceDateRow(int row) const
1096 if (row <= 0)
1097 return 0;
1099 if (m_sourceRowCache.isEmpty())
1100 rowCount(QModelIndex());
1102 if (row >= m_sourceRowCache.count()) {
1103 if (!sourceModel())
1104 return 0;
1105 return sourceModel()->rowCount();
1107 return m_sourceRowCache.at(row);
1110 QModelIndex HistoryTreeModel::mapToSource(const QModelIndex &proxyIndex) const
1112 int offset = proxyIndex.internalId();
1113 if (offset == 0)
1114 return QModelIndex();
1115 int startDateRow = sourceDateRow(offset - 1);
1116 return sourceModel()->index(startDateRow + proxyIndex.row(), proxyIndex.column());
1119 QModelIndex HistoryTreeModel::index(int row, int column, const QModelIndex &parent) const
1121 if (row < 0
1122 || column < 0 || column >= columnCount(parent)
1123 || parent.column() > 0)
1124 return QModelIndex();
1126 if (!parent.isValid())
1127 return createIndex(row, column, 0);
1128 return createIndex(row, column, parent.row() + 1);
1131 QModelIndex HistoryTreeModel::parent(const QModelIndex &index) const
1133 int offset = index.internalId();
1134 if (offset == 0 || !index.isValid())
1135 return QModelIndex();
1136 return createIndex(offset - 1, 0, 0);
1139 bool HistoryTreeModel::hasChildren(const QModelIndex &parent) const
1141 QModelIndex grandparent = parent.parent();
1142 if (!grandparent.isValid())
1143 return true;
1144 return false;
1147 Qt::ItemFlags HistoryTreeModel::flags(const QModelIndex &index) const
1149 if (!index.isValid())
1150 return Qt::NoItemFlags;
1151 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
1154 bool HistoryTreeModel::removeRows(int row, int count, const QModelIndex &parent)
1156 if (row < 0 || count <= 0 || row + count > rowCount(parent))
1157 return false;
1159 if (parent.isValid()) {
1160 // removing pages
1161 int offset = sourceDateRow(parent.row());
1162 return sourceModel()->removeRows(offset + row, count);
1163 } else {
1164 // removing whole dates
1165 for (int i = row + count - 1; i >= row; --i) {
1166 QModelIndex dateParent = index(i, 0);
1167 int offset = sourceDateRow(dateParent.row());
1168 if (!sourceModel()->removeRows(offset, rowCount(dateParent)))
1169 return false;
1172 return true;
1175 void HistoryTreeModel::setSourceModel(QAbstractItemModel *newSourceModel)
1177 if (sourceModel()) {
1178 disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
1179 disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset()));
1180 disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
1181 this, SLOT(sourceRowsInserted(QModelIndex,int,int)));
1182 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
1183 this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));
1186 QAbstractProxyModel::setSourceModel(newSourceModel);
1188 if (newSourceModel) {
1189 connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset()));
1190 connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset()));
1191 connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
1192 this, SLOT(sourceRowsInserted(QModelIndex,int,int)));
1193 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
1194 this, SLOT(sourceRowsRemoved(QModelIndex,int,int)));
1197 reset();
1200 void HistoryTreeModel::sourceReset()
1202 m_sourceRowCache.clear();
1203 reset();
1206 void HistoryTreeModel::sourceRowsInserted(const QModelIndex &parent, int start, int end)
1208 Q_UNUSED(parent); // Avoid warnings when compiling release
1209 Q_ASSERT(!parent.isValid());
1210 if (start != 0 || start != end) {
1211 m_sourceRowCache.clear();
1212 reset();
1213 return;
1216 m_sourceRowCache.clear();
1217 QModelIndex treeIndex = mapFromSource(sourceModel()->index(start, 0));
1218 QModelIndex treeParent = treeIndex.parent();
1219 if (rowCount(treeParent) == 1) {
1220 beginInsertRows(QModelIndex(), 0, 0);
1221 endInsertRows();
1222 } else {
1223 beginInsertRows(treeParent, treeIndex.row(), treeIndex.row());
1224 endInsertRows();
1228 QModelIndex HistoryTreeModel::mapFromSource(const QModelIndex &sourceIndex) const
1230 if (!sourceIndex.isValid())
1231 return QModelIndex();
1233 if (m_sourceRowCache.isEmpty())
1234 rowCount(QModelIndex());
1236 QList<int>::iterator it;
1237 it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), sourceIndex.row());
1238 if (*it != sourceIndex.row())
1239 --it;
1240 int dateRow = qMax(0, it - m_sourceRowCache.begin());
1241 int row = sourceIndex.row() - m_sourceRowCache.at(dateRow);
1242 return createIndex(row, sourceIndex.column(), dateRow + 1);
1245 void HistoryTreeModel::sourceRowsRemoved(const QModelIndex &parent, int start, int end)
1247 Q_UNUSED(parent); // Avoid warnings when compiling release
1248 Q_ASSERT(!parent.isValid());
1249 if (m_sourceRowCache.isEmpty())
1250 return;
1251 for (int i = end; i >= start;) {
1252 QList<int>::iterator it;
1253 it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), i);
1254 // playing it safe
1255 if (it == m_sourceRowCache.end()) {
1256 m_sourceRowCache.clear();
1257 reset();
1258 return;
1261 if (*it != i)
1262 --it;
1263 int row = qMax(0, it - m_sourceRowCache.begin());
1264 int offset = m_sourceRowCache[row];
1265 QModelIndex dateParent = index(row, 0);
1266 // If we can remove all the rows in the date do that and skip over them
1267 int rc = rowCount(dateParent);
1268 if (i - rc + 1 == offset && start <= i - rc + 1) {
1269 beginRemoveRows(QModelIndex(), row, row);
1270 m_sourceRowCache.removeAt(row);
1271 i -= rc + 1;
1272 } else {
1273 beginRemoveRows(dateParent, i - offset, i - offset);
1274 ++row;
1275 --i;
1277 for (int j = row; j < m_sourceRowCache.count(); ++j)
1278 --m_sourceRowCache[j];
1279 endRemoveRows();