Mercurial > projects > qtd
comparison demos/browser/history.d @ 45:71b382c10ef6
add coarse and incomplete QT browser port
author | mandel |
---|---|
date | Sun, 17 May 2009 18:49:59 +0000 |
parents | |
children | a62227112f5b |
comparison
equal
deleted
inserted
replaced
44:3cb15c92ac28 | 45:71b382c10ef6 |
---|---|
1 /**************************************************************************** | |
2 ** | |
3 ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). | |
4 ** Contact: Qt Software Information (qt-info@nokia.com) | |
5 ** | |
6 ** This file is part of the demonstration applications of the Qt Toolkit. | |
7 ** | |
8 ** $QT_BEGIN_LICENSE:LGPL$ | |
9 ** Commercial Usage | |
10 ** Licensees holding valid Qt Commercial licenses may use this file in | |
11 ** accordance with the Qt Commercial License Agreement provided with the | |
12 ** Software or, alternatively, in accordance with the terms contained in | |
13 ** a written agreement between you and Nokia. | |
14 ** | |
15 ** GNU Lesser General Public License Usage | |
16 ** Alternatively, this file may be used under the terms of the GNU Lesser | |
17 ** General Public License version 2.1 as published by the Free Software | |
18 ** Foundation and appearing in the file LICENSE.LGPL included in the | |
19 ** packaging of this file. Please review the following information to | |
20 ** ensure the GNU Lesser General Public License version 2.1 requirements | |
21 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. | |
22 ** | |
23 ** In addition, as a special exception, Nokia gives you certain | |
24 ** additional rights. These rights are described in the Nokia Qt LGPL | |
25 ** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this | |
26 ** package. | |
27 ** | |
28 ** GNU General Public License Usage | |
29 ** Alternatively, this file may be used under the terms of the GNU | |
30 ** General Public License version 3.0 as published by the Free Software | |
31 ** Foundation and appearing in the file LICENSE.GPL included in the | |
32 ** packaging of this file. Please review the following information to | |
33 ** ensure the GNU General Public License version 3.0 requirements will be | |
34 ** met: http://www.gnu.org/copyleft/gpl.html. | |
35 ** | |
36 ** If you are unsure which license is appropriate for your use, please | |
37 ** contact the sales department at qt-sales@nokia.com. | |
38 ** $QT_END_LICENSE$ | |
39 ** | |
40 ****************************************************************************/ | |
41 module history; | |
42 | |
43 #include "autosaver.h" | |
44 #include "browserapplication.h" | |
45 | |
46 import QtCore.QBuffer; | |
47 import QtCore.QDir; | |
48 import QtCore.QFile; | |
49 import QtCore.QFileInfo; | |
50 import QtCore.QSettings; | |
51 import QtCore.QTemporaryFile; | |
52 import QtCore.QTextStream; | |
53 | |
54 import QtCore.QtAlgorithms; | |
55 | |
56 import QtGui.QClipboard; | |
57 import QtGui.QDesktopServices; | |
58 import QtGui.QHeaderView; | |
59 import QtGui.QStyle; | |
60 | |
61 import QtWebKit.QWebHistoryInterface; | |
62 import QtWebKit.QWebSettings; | |
63 | |
64 import QtCore.QDebug; | |
65 | |
66 | |
67 #include "modelmenu.h" | |
68 | |
69 import QtCore.QDateTime; | |
70 import QtCore.QHash; | |
71 import QtCore.QObject; | |
72 import QtCore.QTimer; | |
73 import QtCore.QUrl; | |
74 | |
75 import QtGui.QSortFilterProxyModel; | |
76 | |
77 import QWebHistoryInterface; | |
78 | |
79 static const unsigned int HISTORY_VERSION = 23; | |
80 | |
81 | |
82 class HistoryItem | |
83 { | |
84 public: | |
85 this() {} | |
86 this(const QString &u, const QDateTime &d = QDateTime(), const QString &t = QString()) | |
87 { | |
88 title = t; | |
89 url = u; | |
90 dateTime = d; | |
91 } | |
92 | |
93 inline bool operator==(const HistoryItem &other) | |
94 { return other.title == title | |
95 && other.url == url && other.dateTime == dateTime; | |
96 } | |
97 | |
98 // history is sorted in reverse | |
99 inline bool operator <(const HistoryItem &other) | |
100 { return dateTime > other.dateTime; } | |
101 | |
102 QString title; | |
103 QString url; | |
104 QDateTime dateTime; | |
105 } | |
106 | |
107 /* | |
108 class AutoSaver; | |
109 class HistoryModel; | |
110 class HistoryFilterModel; | |
111 class HistoryTreeModel; | |
112 */ | |
113 | |
114 class HistoryManager : public QWebHistoryInterface | |
115 { | |
116 Q_OBJECT | |
117 Q_PROPERTY(int historyLimit READ historyLimit WRITE setHistoryLimit) | |
118 | |
119 signals: | |
120 void historyReset(); | |
121 void entryAdded(const HistoryItem &item); | |
122 void entryRemoved(const HistoryItem &item); | |
123 void entryUpdated(int offset); | |
124 | |
125 public: | |
126 | |
127 this(QObject *parent = null) | |
128 { | |
129 super(parent); | |
130 m_saveTimer = new AutoSaver(this); | |
131 m_historyLimit = 30; | |
132 m_historyModel = 0; | |
133 m_historyFilterModel = 0; | |
134 m_historyTreeModel = 0; | |
135 | |
136 m_expiredTimer.setSingleShot(true); | |
137 connect(&m_expiredTimer, SIGNAL(timeout()), | |
138 this, SLOT(checkForExpired())); | |
139 connect(this, SIGNAL(entryAdded(const HistoryItem &)), | |
140 m_saveTimer, SLOT(changeOccurred())); | |
141 connect(this, SIGNAL(entryRemoved(const HistoryItem &)), | |
142 m_saveTimer, SLOT(changeOccurred())); | |
143 load(); | |
144 | |
145 m_historyModel = new HistoryModel(this, this); | |
146 m_historyFilterModel = new HistoryFilterModel(m_historyModel, this); | |
147 m_historyTreeModel = new HistoryTreeModel(m_historyFilterModel, this); | |
148 | |
149 // QWebHistoryInterface will delete the history manager | |
150 QWebHistoryInterface::setDefaultInterface(this); | |
151 } | |
152 | |
153 | |
154 ~this() | |
155 { | |
156 m_saveTimer.saveIfNeccessary(); | |
157 } | |
158 | |
159 | |
160 bool historyContains(const QString &url) | |
161 { | |
162 return m_historyFilterModel.historyContains(url); | |
163 } | |
164 | |
165 void addHistoryEntry(QString &url) | |
166 { | |
167 QUrl cleanUrl(url); | |
168 cleanUrl.setPassword(QString()); | |
169 cleanUrl.setHost(cleanUrl.host().toLower()); | |
170 HistoryItem item(cleanUrl.toString(), QDateTime::currentDateTime()); | |
171 addHistoryItem(item); | |
172 } | |
173 | |
174 void updateHistoryItem(QUrl &url, QString &title) | |
175 { | |
176 for (int i = 0; i < m_history.count(); ++i) { | |
177 if (url == m_history.at(i).url) { | |
178 m_history[i].title = title; | |
179 m_saveTimer.changeOccurred(); | |
180 if (m_lastSavedUrl.isEmpty()) | |
181 m_lastSavedUrl = m_history.at(i).url; | |
182 emit entryUpdated(i); | |
183 break; | |
184 } | |
185 } | |
186 } | |
187 | |
188 int historyLimit() | |
189 { | |
190 return m_historyLimit; | |
191 } | |
192 | |
193 | |
194 void setHistoryLimit(int limit) | |
195 { | |
196 if (m_historyLimit == limit) | |
197 return; | |
198 m_historyLimit = limit; | |
199 checkForExpired(); | |
200 m_saveTimer.changeOccurred(); | |
201 } | |
202 | |
203 | |
204 QList<HistoryItem> history() | |
205 { | |
206 return m_history; | |
207 } | |
208 | |
209 | |
210 void setHistory(QList<HistoryItem> &history, bool loadedAndSorted = false); | |
211 { | |
212 m_history = history; | |
213 | |
214 // verify that it is sorted by date | |
215 if (!loadedAndSorted) | |
216 qSort(m_history.begin(), m_history.end()); | |
217 | |
218 checkForExpired(); | |
219 | |
220 if (loadedAndSorted) { | |
221 m_lastSavedUrl = m_history.value(0).url; | |
222 } else { | |
223 m_lastSavedUrl = QString(); | |
224 m_saveTimer.changeOccurred(); | |
225 } | |
226 emit historyReset(); | |
227 } | |
228 | |
229 | |
230 // History manager keeps around these models for use by the completer and other classes | |
231 HistoryModel *historyModel(); | |
232 { | |
233 return m_historyModel; | |
234 } | |
235 | |
236 | |
237 HistoryFilterModel *historyFilterModel() | |
238 { | |
239 return m_historyFilterModel; | |
240 } | |
241 | |
242 | |
243 HistoryTreeModel *historyTreeModel() | |
244 { | |
245 return m_historyTreeModel; | |
246 } | |
247 | |
248 | |
249 public slots: | |
250 | |
251 void clear() | |
252 { | |
253 m_history.clear(); | |
254 m_lastSavedUrl = QString(); | |
255 m_saveTimer.changeOccurred(); | |
256 m_saveTimer.saveIfNeccessary(); | |
257 historyReset(); | |
258 } | |
259 | |
260 void loadSettings() | |
261 { | |
262 // load settings | |
263 QSettings settings; | |
264 settings.beginGroup(QLatin1String("history")); | |
265 m_historyLimit = settings.value(QLatin1String("historyLimit"), 30).toInt(); | |
266 } | |
267 | |
268 private slots: | |
269 void save() | |
270 { | |
271 QSettings settings; | |
272 settings.beginGroup(QLatin1String("history")); | |
273 settings.setValue(QLatin1String("historyLimit"), m_historyLimit); | |
274 | |
275 bool saveAll = m_lastSavedUrl.isEmpty(); | |
276 int first = m_history.count() - 1; | |
277 if (!saveAll) { | |
278 // find the first one to save | |
279 for (int i = 0; i < m_history.count(); ++i) { | |
280 if (m_history.at(i).url == m_lastSavedUrl) { | |
281 first = i - 1; | |
282 break; | |
283 } | |
284 } | |
285 } | |
286 if (first == m_history.count() - 1) | |
287 saveAll = true; | |
288 | |
289 QString directory = QDesktopServices::storageLocation(QDesktopServices::DataLocation); | |
290 if (directory.isEmpty()) | |
291 directory = QDir::homePath() + QLatin1String("/.") + QCoreApplication::applicationName(); | |
292 if (!QFile::exists(directory)) { | |
293 QDir dir; | |
294 dir.mkpath(directory); | |
295 } | |
296 | |
297 QFile historyFile(directory + QLatin1String("/history")); | |
298 // When saving everything use a temporary file to prevent possible data loss. | |
299 QTemporaryFile tempFile; | |
300 tempFile.setAutoRemove(false); | |
301 bool open = false; | |
302 if (saveAll) { | |
303 open = tempFile.open(); | |
304 } else { | |
305 open = historyFile.open(QFile::Append); | |
306 } | |
307 | |
308 if (!open) { | |
309 qWarning() << "Unable to open history file for saving" | |
310 << (saveAll ? tempFile.fileName() : historyFile.fileName()); | |
311 return; | |
312 } | |
313 | |
314 QDataStream out(saveAll ? &tempFile : &historyFile); | |
315 for (int i = first; i >= 0; --i) { | |
316 QByteArray data; | |
317 QDataStream stream(&data, QIODevice::WriteOnly); | |
318 HistoryItem item = m_history.at(i); | |
319 stream << HISTORY_VERSION << item.url << item.dateTime << item.title; | |
320 out << data; | |
321 } | |
322 tempFile.close(); | |
323 | |
324 if (saveAll) { | |
325 if (historyFile.exists() && !historyFile.remove()) | |
326 qWarning() << "History: error removing old history." << historyFile.errorString(); | |
327 if (!tempFile.rename(historyFile.fileName())) | |
328 qWarning() << "History: error moving new history over old." << tempFile.errorString() << historyFile.fileName(); | |
329 } | |
330 m_lastSavedUrl = m_history.value(0).url; | |
331 } | |
332 | |
333 void checkForExpired() | |
334 { | |
335 if (m_historyLimit < 0 || m_history.isEmpty()) | |
336 return; | |
337 | |
338 QDateTime now = QDateTime::currentDateTime(); | |
339 int nextTimeout = 0; | |
340 | |
341 while (!m_history.isEmpty()) { | |
342 QDateTime checkForExpired = m_history.last().dateTime; | |
343 checkForExpired.setDate(checkForExpired.date().addDays(m_historyLimit)); | |
344 if (now.daysTo(checkForExpired) > 7) { | |
345 // check at most in a week to prevent int overflows on the timer | |
346 nextTimeout = 7 * 86400; | |
347 } else { | |
348 nextTimeout = now.secsTo(checkForExpired); | |
349 } | |
350 if (nextTimeout > 0) | |
351 break; | |
352 HistoryItem item = m_history.takeLast(); | |
353 // remove from saved file also | |
354 m_lastSavedUrl = QString(); | |
355 emit entryRemoved(item); | |
356 } | |
357 | |
358 if (nextTimeout > 0) | |
359 m_expiredTimer.start(nextTimeout * 1000); | |
360 } | |
361 | |
362 protected: | |
363 void addHistoryItem(HistoryItem &item) | |
364 { | |
365 QWebSettings *globalSettings = QWebSettings::globalSettings(); | |
366 if (globalSettings.testAttribute(QWebSettings::PrivateBrowsingEnabled)) | |
367 return; | |
368 | |
369 m_history.prepend(item); | |
370 emit entryAdded(item); | |
371 if (m_history.count() == 1) | |
372 checkForExpired(); | |
373 } | |
374 | |
375 private: | |
376 | |
377 void load() | |
378 { | |
379 loadSettings(); | |
380 | |
381 QFile historyFile(QDesktopServices::storageLocation(QDesktopServices::DataLocation) | |
382 + QLatin1String("/history")); | |
383 if (!historyFile.exists()) | |
384 return; | |
385 if (!historyFile.open(QFile::ReadOnly)) { | |
386 qWarning() << "Unable to open history file" << historyFile.fileName(); | |
387 return; | |
388 } | |
389 | |
390 QList<HistoryItem> list; | |
391 QDataStream in(&historyFile); | |
392 // Double check that the history file is sorted as it is read in | |
393 bool needToSort = false; | |
394 HistoryItem lastInsertedItem; | |
395 QByteArray data; | |
396 QDataStream stream; | |
397 QBuffer buffer; | |
398 stream.setDevice(&buffer); | |
399 while (!historyFile.atEnd()) { | |
400 in >> data; | |
401 buffer.close(); | |
402 buffer.setBuffer(&data); | |
403 buffer.open(QIODevice::ReadOnly); | |
404 quint32 ver; | |
405 stream >> ver; | |
406 if (ver != HISTORY_VERSION) | |
407 continue; | |
408 HistoryItem item; | |
409 stream >> item.url; | |
410 stream >> item.dateTime; | |
411 stream >> item.title; | |
412 | |
413 if (!item.dateTime.isValid()) | |
414 continue; | |
415 | |
416 if (item == lastInsertedItem) { | |
417 if (lastInsertedItem.title.isEmpty() && !list.isEmpty()) | |
418 list[0].title = item.title; | |
419 continue; | |
420 } | |
421 | |
422 if (!needToSort && !list.isEmpty() && lastInsertedItem < item) | |
423 needToSort = true; | |
424 | |
425 list.prepend(item); | |
426 lastInsertedItem = item; | |
427 } | |
428 if (needToSort) | |
429 qSort(list.begin(), list.end()); | |
430 | |
431 setHistory(list, true); | |
432 | |
433 // If we had to sort re-write the whole history sorted | |
434 if (needToSort) { | |
435 m_lastSavedUrl = QString(); | |
436 m_saveTimer.changeOccurred(); | |
437 } | |
438 } | |
439 | |
440 AutoSaver *m_saveTimer; | |
441 int m_historyLimit; | |
442 QTimer m_expiredTimer; | |
443 QList<HistoryItem> m_history; | |
444 QString m_lastSavedUrl; | |
445 | |
446 HistoryModel *m_historyModel; | |
447 HistoryFilterModel *m_historyFilterModel; | |
448 HistoryTreeModel *m_historyTreeModel; | |
449 }; | |
450 | |
451 class HistoryModel : public QAbstractTableModel | |
452 { | |
453 Q_OBJECT | |
454 | |
455 public slots: | |
456 void historyReset() | |
457 { | |
458 reset(); | |
459 } | |
460 | |
461 void entryAdded() | |
462 { | |
463 beginInsertRows(QModelIndex(), 0, 0); | |
464 endInsertRows(); | |
465 } | |
466 | |
467 void entryUpdated(int offset) | |
468 { | |
469 QModelIndex idx = index(offset, 0); | |
470 emit dataChanged(idx, idx); | |
471 } | |
472 | |
473 public: | |
474 | |
475 enum Roles { | |
476 DateRole = Qt.UserRole + 1, | |
477 DateTimeRole = Qt.UserRole + 2, | |
478 UrlRole = Qt.UserRole + 3, | |
479 UrlStringRole = Qt.UserRole + 4 | |
480 }; | |
481 | |
482 this(HistoryManager *history, QObject *parent = null) | |
483 { | |
484 super(parent); | |
485 m_history = history; | |
486 | |
487 Q_ASSERT(m_history); | |
488 connect(m_history, SIGNAL(historyReset()), | |
489 this, SLOT(historyReset())); | |
490 connect(m_history, SIGNAL(entryRemoved(HistoryItem &)), | |
491 this, SLOT(historyReset())); | |
492 | |
493 connect(m_history, SIGNAL(entryAdded(HistoryItem &)), | |
494 this, SLOT(entryAdded())); | |
495 connect(m_history, SIGNAL(entryUpdated(int)), | |
496 this, SLOT(entryUpdated(int))); | |
497 } | |
498 | |
499 QVariant headerData(int section, Qt.Orientation orientation, int role = Qt.DisplayRole) | |
500 { | |
501 if (orientation == Qt.Horizontal && role == Qt.DisplayRole) { | |
502 switch (section) { | |
503 case 0: return tr("Title"); | |
504 case 1: return tr("Address"); | |
505 } | |
506 } | |
507 return QAbstractTableModel::headerData(section, orientation, role); | |
508 } | |
509 | |
510 QVariant data(QModelIndex &index, int role = Qt.DisplayRole) | |
511 { | |
512 QList<HistoryItem> lst = m_history.history(); | |
513 if (index.row() < 0 || index.row() >= lst.size()) | |
514 return QVariant(); | |
515 | |
516 const HistoryItem &item = lst.at(index.row()); | |
517 switch (role) { | |
518 case DateTimeRole: | |
519 return item.dateTime; | |
520 case DateRole: | |
521 return item.dateTime.date(); | |
522 case UrlRole: | |
523 return QUrl(item.url); | |
524 case UrlStringRole: | |
525 return item.url; | |
526 case Qt.DisplayRole: | |
527 case Qt.EditRole: { | |
528 switch (index.column()) { | |
529 case 0: | |
530 // when there is no title try to generate one from the url | |
531 if (item.title.isEmpty()) { | |
532 QString page = QFileInfo(QUrl(item.url).path()).fileName(); | |
533 if (!page.isEmpty()) | |
534 return page; | |
535 return item.url; | |
536 } | |
537 return item.title; | |
538 case 1: | |
539 return item.url; | |
540 } | |
541 } | |
542 case Qt.DecorationRole: | |
543 if (index.column() == 0) { | |
544 return BrowserApplication::instance().icon(item.url); | |
545 } | |
546 } | |
547 return QVariant(); | |
548 } | |
549 | |
550 int columnCount(QModelIndex &parent = QModelIndex()) | |
551 { | |
552 return (parent.isValid()) ? 0 : 2; | |
553 } | |
554 | |
555 int rowCount(QModelIndex &parent = QModelIndex()) | |
556 { | |
557 return (parent.isValid()) ? 0 : m_history.history().count(); | |
558 } | |
559 | |
560 bool removeRows(int row, int count, QModelIndex &parent = QModelIndex()) | |
561 { | |
562 if (parent.isValid()) | |
563 return false; | |
564 int lastRow = row + count - 1; | |
565 beginRemoveRows(parent, row, lastRow); | |
566 QList<HistoryItem> lst = m_history.history(); | |
567 for (int i = lastRow; i >= row; --i) | |
568 lst.removeAt(i); | |
569 disconnect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset())); | |
570 m_history.setHistory(lst); | |
571 connect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset())); | |
572 endRemoveRows(); | |
573 return true; | |
574 } | |
575 | |
576 | |
577 private: | |
578 HistoryManager *m_history; | |
579 } | |
580 | |
581 const uint MOVEDROWS = 15; | |
582 | |
583 /*! | |
584 Proxy model that will remove any duplicate entries. | |
585 Both m_sourceRow and m_historyHash store their offsets not from | |
586 the front of the list, but as offsets from the back. | |
587 */ | |
588 class HistoryFilterModel : public QAbstractProxyModel | |
589 { | |
590 Q_OBJECT | |
591 | |
592 public: | |
593 | |
594 this(QAbstractItemModel *sourceModel, QObject *parent = null) | |
595 { | |
596 super(parent); | |
597 m_loaded = false; | |
598 setSourceModel(sourceModel); | |
599 } | |
600 | |
601 inline bool historyContains(QString &url) | |
602 { | |
603 load(); | |
604 return m_historyHash.contains(url); | |
605 } | |
606 | |
607 int historyLocation(QString &url); | |
608 { | |
609 load(); | |
610 if (!m_historyHash.contains(url)) | |
611 return 0; | |
612 return sourceModel().rowCount() - m_historyHash.value(url); | |
613 } | |
614 | |
615 QModelIndex mapFromSource(QModelIndex &sourceIndex) | |
616 { | |
617 load(); | |
618 QString url = sourceIndex.data(HistoryModel.UrlStringRole).toString(); | |
619 if (!m_historyHash.contains(url)) | |
620 return QModelIndex(); | |
621 | |
622 // This can be done in a binary search, but we can't use qBinary find | |
623 // because it can't take: qBinaryFind(m_sourceRow.end(), m_sourceRow.begin(), v); | |
624 // so if this is a performance bottlneck then convert to binary search, until then | |
625 // the cleaner/easier to read code wins the day. | |
626 int realRow = -1; | |
627 int sourceModelRow = sourceModel().rowCount() - sourceIndex.row(); | |
628 | |
629 for (int i = 0; i < m_sourceRow.count(); ++i) { | |
630 if (m_sourceRow.at(i) == sourceModelRow) { | |
631 realRow = i; | |
632 break; | |
633 } | |
634 } | |
635 if (realRow == -1) | |
636 return QModelIndex(); | |
637 | |
638 return createIndex(realRow, sourceIndex.column(), sourceModel().rowCount() - sourceIndex.row()); | |
639 } | |
640 | |
641 | |
642 QModelIndex mapToSource(QModelIndex &proxyIndex) | |
643 { | |
644 load(); | |
645 int sourceRow = sourceModel().rowCount() - proxyIndex.internalId(); | |
646 return sourceModel().index(sourceRow, proxyIndex.column()); | |
647 } | |
648 | |
649 void setSourceModel(QAbstractItemModel *newSourceModel) | |
650 { | |
651 if (sourceModel()) { | |
652 disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); | |
653 disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex &, QModelIndex &)), | |
654 this, SLOT(dataChanged(QModelIndex &, QModelIndex &))); | |
655 disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex &, int, int)), | |
656 this, SLOT(sourceRowsInserted(QModelIndex &, int, int))); | |
657 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
658 this, SLOT(sourceRowsRemoved(QModelIndex &, int, int))); | |
659 } | |
660 | |
661 QAbstractProxyModel::setSourceModel(newSourceModel); | |
662 | |
663 if (sourceModel()) { | |
664 m_loaded = false; | |
665 connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); | |
666 connect(sourceModel(), SIGNAL(dataChanged(QModelIndex &, QModelIndex &)), | |
667 this, SLOT(sourceDataChanged(QModelIndex &, QModelIndex &))); | |
668 connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex &, int, int)), | |
669 this, SLOT(sourceRowsInserted(QModelIndex &, int, int))); | |
670 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
671 this, SLOT(sourceRowsRemoved(QModelIndex &, int, int))); | |
672 } | |
673 } | |
674 | |
675 QVariant headerData(int section, Qt.Orientation orientation, int role = Qt.DisplayRole); | |
676 { | |
677 return sourceModel().headerData(section, orientation, role); | |
678 } | |
679 | |
680 int rowCount(QModelIndex &parent = QModelIndex()) | |
681 { | |
682 load(); | |
683 if (parent.isValid()) | |
684 return 0; | |
685 return m_historyHash.count(); | |
686 } | |
687 | |
688 int columnCount(QModelIndex &parent = QModelIndex()); | |
689 { | |
690 return (parent.isValid()) ? 0 : 2; | |
691 } | |
692 | |
693 QModelIndex index(int, int, QModelIndex& = QModelIndex()) | |
694 { | |
695 load(); | |
696 if (row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount(parent)) | |
697 return QModelIndex(); | |
698 | |
699 return createIndex(row, column, m_sourceRow[row]); | |
700 } | |
701 | |
702 | |
703 QModelIndex parent(QModelIndex& index= QModelIndex()) | |
704 { | |
705 return QModelIndex(); | |
706 } | |
707 | |
708 /* | |
709 Removing a continuous block of rows will remove filtered rows too as this is | |
710 the users intention. | |
711 */ | |
712 bool removeRows(int row, int count, QModelIndex &parent = QModelIndex()); | |
713 { | |
714 if (row < 0 || count <= 0 || row + count > rowCount(parent) || parent.isValid()) | |
715 return false; | |
716 int lastRow = row + count - 1; | |
717 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
718 this, SLOT(sourceRowsRemoved(QModelIndex &, int, int))); | |
719 beginRemoveRows(parent, row, lastRow); | |
720 int oldCount = rowCount(); | |
721 int start = sourceModel().rowCount() - m_sourceRow.value(row); | |
722 int end = sourceModel().rowCount() - m_sourceRow.value(lastRow); | |
723 sourceModel().removeRows(start, end - start + 1); | |
724 endRemoveRows(); | |
725 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
726 this, SLOT(sourceRowsRemoved(QModelIndex &, int, int))); | |
727 m_loaded = false; | |
728 if (oldCount - count != rowCount()) | |
729 reset(); | |
730 return true; | |
731 } | |
732 | |
733 | |
734 QVariant data(QModelIndex &index, int role = Qt.DisplayRole); | |
735 { | |
736 return QAbstractProxyModel::data(index, role); | |
737 } | |
738 | |
739 private slots: | |
740 | |
741 void sourceReset() | |
742 { | |
743 m_loaded = false; | |
744 reset(); | |
745 } | |
746 | |
747 | |
748 void sourceDataChanged(QModelIndex &topLeft, QModelIndex &bottomRight) | |
749 { | |
750 emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight)); | |
751 } | |
752 | |
753 void sourceRowsRemoved(QModelIndex &, int start, int end) | |
754 { | |
755 Q_UNUSED(start); | |
756 Q_UNUSED(end); | |
757 sourceReset(); | |
758 } | |
759 | |
760 | |
761 void sourceRowsInserted(QModelIndex &parent, int start, int end) | |
762 { | |
763 Q_ASSERT(start == end && start == 0); | |
764 Q_UNUSED(end); | |
765 if (!m_loaded) | |
766 return; | |
767 QModelIndex idx = sourceModel().index(start, 0, parent); | |
768 QString url = idx.data(HistoryModel.UrlStringRole).toString(); | |
769 if (m_historyHash.contains(url)) { | |
770 int sourceRow = sourceModel().rowCount() - m_historyHash[url]; | |
771 int realRow = mapFromSource(sourceModel().index(sourceRow, 0)).row(); | |
772 beginRemoveRows(QModelIndex(), realRow, realRow); | |
773 m_sourceRow.removeAt(realRow); | |
774 m_historyHash.remove(url); | |
775 endRemoveRows(); | |
776 } | |
777 beginInsertRows(QModelIndex(), 0, 0); | |
778 m_historyHash.insert(url, sourceModel().rowCount() - start); | |
779 m_sourceRow.insert(0, sourceModel().rowCount()); | |
780 endInsertRows(); | |
781 } | |
782 | |
783 private: | |
784 void load() | |
785 { | |
786 if (m_loaded) | |
787 return; | |
788 m_sourceRow.clear(); | |
789 m_historyHash.clear(); | |
790 m_historyHash.reserve(sourceModel().rowCount()); | |
791 for (int i = 0; i < sourceModel().rowCount(); ++i) { | |
792 QModelIndex idx = sourceModel().index(i, 0); | |
793 QString url = idx.data(HistoryModel.UrlStringRole).toString(); | |
794 if (!m_historyHash.contains(url)) { | |
795 m_sourceRow.append(sourceModel().rowCount() - i); | |
796 m_historyHash[url] = sourceModel().rowCount() - i; | |
797 } | |
798 } | |
799 m_loaded = true; | |
800 } | |
801 | |
802 | |
803 mutable QList<int> m_sourceRow; | |
804 mutable QHash<QString, int> m_historyHash; | |
805 mutable bool m_loaded; | |
806 } | |
807 | |
808 /* | |
809 The history menu | |
810 - Removes the first twenty entries and puts them as children of the top level. | |
811 - If there are less then twenty entries then the first folder is also removed. | |
812 | |
813 The mapping is done by knowing that HistoryTreeModel is over a table | |
814 We store that row offset in our index's private data. | |
815 */ | |
816 class HistoryMenuModel : public QAbstractProxyModel | |
817 { | |
818 Q_OBJECT | |
819 | |
820 public: | |
821 | |
822 /* | |
823 Maps the first bunch of items of the source model to the root | |
824 */ | |
825 HistoryMenuModel(HistoryTreeModel *sourceModel, QObject *parent = null) | |
826 { | |
827 super(parent); | |
828 m_treeModel = sourceModel; | |
829 setSourceModel(sourceModel); | |
830 } | |
831 | |
832 int columnCount(QModelIndex &parent) | |
833 { | |
834 return m_treeModel.columnCount(mapToSource(parent)); | |
835 } | |
836 | |
837 int rowCount(QModelIndex &parent = QModelIndex()); | |
838 { | |
839 if (parent.column() > 0) | |
840 return 0; | |
841 | |
842 if (!parent.isValid()) { | |
843 int folders = sourceModel().rowCount(); | |
844 int bumpedItems = bumpedRows(); | |
845 if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel().rowCount(sourceModel().index(0, 0))) | |
846 --folders; | |
847 return bumpedItems + folders; | |
848 } | |
849 | |
850 if (parent.internalId() == -1) { | |
851 if (parent.row() < bumpedRows()) | |
852 return 0; | |
853 } | |
854 | |
855 QModelIndex idx = mapToSource(parent); | |
856 int defaultCount = sourceModel().rowCount(idx); | |
857 if (idx == sourceModel().index(0, 0)) | |
858 return defaultCount - bumpedRows(); | |
859 return defaultCount; | |
860 } | |
861 | |
862 QModelIndex mapFromSource(QModelIndex &sourceIndex) | |
863 { | |
864 // currently not used or autotested | |
865 Q_ASSERT(false); | |
866 int sr = m_treeModel.mapToSource(sourceIndex).row(); | |
867 return createIndex(sourceIndex.row(), sourceIndex.column(), sr); | |
868 } | |
869 | |
870 QModelIndex mapToSource(QModelIndex &proxyIndex) | |
871 { | |
872 if (!proxyIndex.isValid()) | |
873 return QModelIndex(); | |
874 | |
875 if (proxyIndex.internalId() == -1) { | |
876 int bumpedItems = bumpedRows(); | |
877 if (proxyIndex.row() < bumpedItems) | |
878 return m_treeModel.index(proxyIndex.row(), proxyIndex.column(), m_treeModel.index(0, 0)); | |
879 if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel().rowCount(m_treeModel.index(0, 0))) | |
880 --bumpedItems; | |
881 return m_treeModel.index(proxyIndex.row() - bumpedItems, proxyIndex.column()); | |
882 } | |
883 | |
884 QModelIndex historyIndex = m_treeModel.sourceModel().index(proxyIndex.internalId(), proxyIndex.column()); | |
885 QModelIndex treeIndex = m_treeModel.mapFromSource(historyIndex); | |
886 return treeIndex; | |
887 } | |
888 | |
889 | |
890 QModelIndex index(int, int, QModelIndex &parent = QModelIndex()); | |
891 { | |
892 if (row < 0 | |
893 || column < 0 || column >= columnCount(parent) | |
894 || parent.column() > 0) | |
895 return QModelIndex(); | |
896 if (!parent.isValid()) | |
897 return createIndex(row, column, -1); | |
898 | |
899 QModelIndex treeIndexParent = mapToSource(parent); | |
900 | |
901 int bumpedItems = 0; | |
902 if (treeIndexParent == m_treeModel.index(0, 0)) | |
903 bumpedItems = bumpedRows(); | |
904 QModelIndex treeIndex = m_treeModel.index(row + bumpedItems, column, treeIndexParent); | |
905 QModelIndex historyIndex = m_treeModel.mapToSource(treeIndex); | |
906 int historyRow = historyIndex.row(); | |
907 if (historyRow == -1) | |
908 historyRow = treeIndex.row(); | |
909 return createIndex(row, column, historyRow); | |
910 } | |
911 | |
912 QModelIndex parent(QModelIndex &index = QModelIndex()); | |
913 { | |
914 int offset = index.internalId(); | |
915 if (offset == -1 || !index.isValid()) | |
916 return QModelIndex(); | |
917 | |
918 QModelIndex historyIndex = m_treeModel.sourceModel().index(index.internalId(), 0); | |
919 QModelIndex treeIndex = m_treeModel.mapFromSource(historyIndex); | |
920 QModelIndex treeIndexParent = treeIndex.parent(); | |
921 | |
922 int sr = m_treeModel.mapToSource(treeIndexParent).row(); | |
923 int bumpedItems = bumpedRows(); | |
924 if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel().rowCount(sourceModel().index(0, 0))) | |
925 --bumpedItems; | |
926 return createIndex(bumpedItems + treeIndexParent.row(), treeIndexParent.column(), sr); | |
927 } | |
928 | |
929 int bumpedRows() | |
930 { | |
931 QModelIndex first = m_treeModel.index(0, 0); | |
932 if (!first.isValid()) | |
933 return 0; | |
934 return qMin(m_treeModel.rowCount(first), MOVEDROWS); | |
935 } | |
936 | |
937 private: | |
938 | |
939 HistoryTreeModel *m_treeModel; | |
940 } | |
941 | |
942 // Menu that is dynamically populated from the history | |
943 class HistoryMenu : public ModelMenu | |
944 { | |
945 Q_OBJECT | |
946 | |
947 signals: | |
948 void openUrl(QUrl &url); | |
949 | |
950 public: | |
951 | |
952 this(QWidget *parent = null) | |
953 { | |
954 super(parent); | |
955 m_history = 0; | |
956 connect(this, SIGNAL(activated(QModelIndex &)), | |
957 this, SLOT(activated(QModelIndex &))); | |
958 setHoverRole(HistoryModel.UrlStringRole); | |
959 } | |
960 | |
961 void setInitialActions(QList<QAction*> actions) | |
962 { | |
963 m_initialActions = actions; | |
964 for (int i = 0; i < m_initialActions.count(); ++i) | |
965 addAction(m_initialActions.at(i)); | |
966 } | |
967 | |
968 protected: | |
969 | |
970 bool prePopulated() | |
971 { | |
972 if (!m_history) { | |
973 m_history = BrowserApplication::historyManager(); | |
974 m_historyMenuModel = new HistoryMenuModel(m_history.historyTreeModel(), this); | |
975 setModel(m_historyMenuModel); | |
976 } | |
977 // initial actions | |
978 for (int i = 0; i < m_initialActions.count(); ++i) | |
979 addAction(m_initialActions.at(i)); | |
980 if (!m_initialActions.isEmpty()) | |
981 addSeparator(); | |
982 setFirstSeparator(m_historyMenuModel.bumpedRows()); | |
983 | |
984 return false; | |
985 } | |
986 | |
987 void postPopulated() | |
988 { | |
989 if (m_history.history().count() > 0) | |
990 addSeparator(); | |
991 | |
992 QAction *showAllAction = new QAction(tr("Show All History"), this); | |
993 connect(showAllAction, SIGNAL(triggered()), this, SLOT(showHistoryDialog())); | |
994 addAction(showAllAction); | |
995 | |
996 QAction *clearAction = new QAction(tr("Clear History"), this); | |
997 connect(clearAction, SIGNAL(triggered()), m_history, SLOT(clear())); | |
998 addAction(clearAction); | |
999 } | |
1000 | |
1001 private slots: | |
1002 void activated(QModelIndex &index) | |
1003 { | |
1004 emit openUrl(index.data(HistoryModel.UrlRole).toUrl()); | |
1005 } | |
1006 | |
1007 void showHistoryDialog() | |
1008 { | |
1009 HistoryDialog *dialog = new HistoryDialog(this); | |
1010 connect(dialog, SIGNAL(openUrl(QUrl&)), | |
1011 this, SIGNAL(openUrl(QUrl&))); | |
1012 dialog.show(); | |
1013 } | |
1014 | |
1015 | |
1016 private: | |
1017 | |
1018 HistoryManager *m_history; | |
1019 HistoryMenuModel *m_historyMenuModel; | |
1020 QList<QAction*> m_initialActions; | |
1021 } | |
1022 | |
1023 // proxy model for the history model that | |
1024 // exposes each url http://www.foo.com and it url starting at the host www.foo.com | |
1025 class HistoryCompletionModel : public QAbstractProxyModel | |
1026 { | |
1027 Q_OBJECT | |
1028 | |
1029 public: | |
1030 this(QObject *parent = null) | |
1031 { | |
1032 super(parent); | |
1033 } | |
1034 | |
1035 QVariant data(QModelIndex &index, int role) | |
1036 { | |
1037 if (sourceModel() | |
1038 && (role == Qt.EditRole || role == Qt.DisplayRole) | |
1039 && index.isValid()) { | |
1040 QModelIndex idx = mapToSource(index); | |
1041 idx = idx.sibling(idx.row(), 1); | |
1042 QString urlString = idx.data(HistoryModel.UrlStringRole).toString(); | |
1043 if (index.row() % 2) { | |
1044 QUrl url = urlString; | |
1045 QString s = url.toString(QUrl::RemoveScheme | QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); | |
1046 return s.mid(2); // strip // from the front | |
1047 } | |
1048 return urlString; | |
1049 } | |
1050 return QAbstractProxyModel::data(index, role); | |
1051 } | |
1052 | |
1053 int rowCount(QModelIndex &parent = QModelIndex()) | |
1054 { | |
1055 return (parent.isValid() || !sourceModel()) ? 0 : sourceModel().rowCount(parent) * 2; | |
1056 } | |
1057 | |
1058 int columnCount(QModelIndex &parent = QModelIndex()) | |
1059 { | |
1060 return (parent.isValid()) ? 0 : 1; | |
1061 } | |
1062 | |
1063 QModelIndex mapFromSource(QModelIndex &sourceIndex) | |
1064 { | |
1065 int row = sourceIndex.row() * 2; | |
1066 return index(row, sourceIndex.column()); | |
1067 } | |
1068 | |
1069 | |
1070 QModelIndex mapToSource(QModelIndex &proxyIndex) | |
1071 { | |
1072 if (!sourceModel()) | |
1073 return QModelIndex(); | |
1074 int row = proxyIndex.row() / 2; | |
1075 return sourceModel().index(row, proxyIndex.column()); | |
1076 } | |
1077 | |
1078 QModelIndex index(int, int, QModelIndex& = QModelIndex()) | |
1079 { | |
1080 if (row < 0 || row >= rowCount(parent) | |
1081 || column < 0 || column >= columnCount(parent)) | |
1082 return QModelIndex(); | |
1083 return createIndex(row, column, 0); | |
1084 } | |
1085 | |
1086 QModelIndex parent(QModelIndex& index= QModelIndex()); | |
1087 { | |
1088 return QModelIndex(); | |
1089 } | |
1090 | |
1091 void setSourceModel(QAbstractItemModel *sourceModel); | |
1092 { | |
1093 if (sourceModel()) { | |
1094 disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); | |
1095 disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex &, int, int)), | |
1096 this, SLOT(sourceReset())); | |
1097 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
1098 this, SLOT(sourceReset())); | |
1099 } | |
1100 | |
1101 QAbstractProxyModel::setSourceModel(newSourceModel); | |
1102 | |
1103 if (newSourceModel) { | |
1104 connect(newSourceModel, SIGNAL(modelReset()), this, SLOT(sourceReset())); | |
1105 connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex &, int, int)), | |
1106 this, SLOT(sourceReset())); | |
1107 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
1108 this, SLOT(sourceReset())); | |
1109 } | |
1110 | |
1111 reset(); | |
1112 } | |
1113 | |
1114 private slots: | |
1115 | |
1116 void sourceReset() | |
1117 { | |
1118 reset(); | |
1119 } | |
1120 } | |
1121 | |
1122 // proxy model for the history model that converts the list | |
1123 // into a tree, one top level node per day. | |
1124 // Used in the HistoryDialog. | |
1125 class HistoryTreeModel : public QAbstractProxyModel | |
1126 { | |
1127 Q_OBJECT | |
1128 | |
1129 public: | |
1130 | |
1131 this(QAbstractItemModel *sourceModel, QObject *parent = null) | |
1132 { | |
1133 super(parent); | |
1134 setSourceModel(sourceModel); | |
1135 } | |
1136 | |
1137 QVariant data(QModelIndex &index, int role = Qt.DisplayRole); | |
1138 { | |
1139 if ((role == Qt.EditRole || role == Qt.DisplayRole)) { | |
1140 int start = index.internalId(); | |
1141 if (start == 0) { | |
1142 int offset = sourceDateRow(index.row()); | |
1143 if (index.column() == 0) { | |
1144 QModelIndex idx = sourceModel().index(offset, 0); | |
1145 QDate date = idx.data(HistoryModel.DateRole).toDate(); | |
1146 if (date == QDate::currentDate()) | |
1147 return tr("Earlier Today"); | |
1148 return date.toString(QLatin1String("dddd, MMMM d, yyyy")); | |
1149 } | |
1150 if (index.column() == 1) { | |
1151 return tr("%1 items").arg(rowCount(index.sibling(index.row(), 0))); | |
1152 } | |
1153 } | |
1154 } | |
1155 if (role == Qt.DecorationRole && index.column() == 0 && !index.parent().isValid()) | |
1156 return QIcon(QLatin1String(":history.png")); | |
1157 if (role == HistoryModel.DateRole && index.column() == 0 && index.internalId() == 0) { | |
1158 int offset = sourceDateRow(index.row()); | |
1159 QModelIndex idx = sourceModel().index(offset, 0); | |
1160 return idx.data(HistoryModel.DateRole); | |
1161 } | |
1162 | |
1163 return QAbstractProxyModel::data(index, role); | |
1164 } | |
1165 | |
1166 | |
1167 int columnCount(QModelIndex &parent); | |
1168 { | |
1169 return sourceModel().columnCount(mapToSource(parent)); | |
1170 } | |
1171 | |
1172 | |
1173 int rowCount(QModelIndex &parent = QModelIndex()) | |
1174 { | |
1175 if ( parent.internalId() != 0 | |
1176 || parent.column() > 0 | |
1177 || !sourceModel()) | |
1178 return 0; | |
1179 | |
1180 // row count OF dates | |
1181 if (!parent.isValid()) { | |
1182 if (!m_sourceRowCache.isEmpty()) | |
1183 return m_sourceRowCache.count(); | |
1184 QDate currentDate; | |
1185 int rows = 0; | |
1186 int totalRows = sourceModel().rowCount(); | |
1187 | |
1188 for (int i = 0; i < totalRows; ++i) { | |
1189 QDate rowDate = sourceModel().index(i, 0).data(HistoryModel.DateRole).toDate(); | |
1190 if (rowDate != currentDate) { | |
1191 m_sourceRowCache.append(i); | |
1192 currentDate = rowDate; | |
1193 ++rows; | |
1194 } | |
1195 } | |
1196 Q_ASSERT(m_sourceRowCache.count() == rows); | |
1197 return rows; | |
1198 } | |
1199 | |
1200 // row count FOR a date | |
1201 int start = sourceDateRow(parent.row()); | |
1202 int end = sourceDateRow(parent.row() + 1); | |
1203 return (end - start); | |
1204 } | |
1205 | |
1206 | |
1207 QModelIndex mapFromSource(QModelIndex &sourceIndex); | |
1208 { | |
1209 if (!sourceIndex.isValid()) | |
1210 return QModelIndex(); | |
1211 | |
1212 if (m_sourceRowCache.isEmpty()) | |
1213 rowCount(QModelIndex()); | |
1214 | |
1215 QList<int>::iterator it; | |
1216 it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), sourceIndex.row()); | |
1217 if (*it != sourceIndex.row()) | |
1218 --it; | |
1219 int dateRow = qMax(0, it - m_sourceRowCache.begin()); | |
1220 int row = sourceIndex.row() - m_sourceRowCache.at(dateRow); | |
1221 return createIndex(row, sourceIndex.column(), dateRow + 1); | |
1222 } | |
1223 | |
1224 | |
1225 QModelIndex mapToSource(QModelIndex &proxyIndex) | |
1226 { | |
1227 int offset = proxyIndex.internalId(); | |
1228 if (offset == 0) | |
1229 return QModelIndex(); | |
1230 int startDateRow = sourceDateRow(offset - 1); | |
1231 return sourceModel().index(startDateRow + proxyIndex.row(), proxyIndex.column()); | |
1232 } | |
1233 | |
1234 | |
1235 QModelIndex index(int row, int column, QModelIndex &parent = QModelIndex()) | |
1236 { | |
1237 if (row < 0 || column < 0 || column >= columnCount(parent) || parent.column() > 0) | |
1238 return QModelIndex(); | |
1239 | |
1240 if (!parent.isValid()) | |
1241 return createIndex(row, column, 0); | |
1242 return createIndex(row, column, parent.row() + 1); | |
1243 } | |
1244 | |
1245 | |
1246 QModelIndex parent(QModelIndex &index= QModelIndex()) | |
1247 { | |
1248 int offset = index.internalId(); | |
1249 if (offset == 0 || !index.isValid()) | |
1250 return QModelIndex(); | |
1251 return createIndex(offset - 1, 0, 0); | |
1252 } | |
1253 | |
1254 | |
1255 bool hasChildren(QModelIndex &parent = QModelIndex()) | |
1256 { | |
1257 QModelIndex grandparent = parent.parent(); | |
1258 if (!grandparent.isValid()) | |
1259 return true; | |
1260 return false; | |
1261 } | |
1262 | |
1263 | |
1264 Qt.ItemFlags flags(QModelIndex &index) | |
1265 { | |
1266 if (!index.isValid()) | |
1267 return Qt.NoItemFlags; | |
1268 return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled; | |
1269 } | |
1270 | |
1271 bool removeRows(int row, int count, QModelIndex &parent = QModelIndex()); | |
1272 { | |
1273 if (row < 0 || count <= 0 || row + count > rowCount(parent)) | |
1274 return false; | |
1275 | |
1276 if (parent.isValid()) { | |
1277 // removing pages | |
1278 int offset = sourceDateRow(parent.row()); | |
1279 return sourceModel().removeRows(offset + row, count); | |
1280 } else { | |
1281 // removing whole dates | |
1282 for (int i = row + count - 1; i >= row; --i) { | |
1283 QModelIndex dateParent = index(i, 0); | |
1284 int offset = sourceDateRow(dateParent.row()); | |
1285 if (!sourceModel().removeRows(offset, rowCount(dateParent))) | |
1286 return false; | |
1287 } | |
1288 } | |
1289 return true; | |
1290 } | |
1291 | |
1292 | |
1293 QVariant headerData(int section, Qt.Orientation orientation, int role = Qt.DisplayRole) | |
1294 { | |
1295 return sourceModel().headerData(section, orientation, role); | |
1296 } | |
1297 | |
1298 | |
1299 | |
1300 void setSourceModel(QAbstractItemModel *newSourceModel) | |
1301 { | |
1302 if (sourceModel()) { | |
1303 disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); | |
1304 disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset())); | |
1305 disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex &, int, int)), | |
1306 this, SLOT(sourceRowsInserted(QModelIndex &, int, int))); | |
1307 disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
1308 this, SLOT(sourceRowsRemoved(QModelIndex &, int, int))); | |
1309 } | |
1310 | |
1311 QAbstractProxyModel::setSourceModel(newSourceModel); | |
1312 | |
1313 if (newSourceModel) { | |
1314 connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); | |
1315 connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset())); | |
1316 connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex &, int, int)), | |
1317 this, SLOT(sourceRowsInserted(QModelIndex &, int, int))); | |
1318 connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex &, int, int)), | |
1319 this, SLOT(sourceRowsRemoved(QModelIndex &, int, int))); | |
1320 } | |
1321 | |
1322 reset(); | |
1323 } | |
1324 | |
1325 | |
1326 private slots: | |
1327 | |
1328 void sourceReset() | |
1329 { | |
1330 m_sourceRowCache.clear(); | |
1331 reset(); | |
1332 } | |
1333 | |
1334 void sourceRowsInserted(QModelIndex &parent, int start, int end); | |
1335 { | |
1336 Q_UNUSED(parent); // Avoid warnings when compiling release | |
1337 Q_ASSERT(!parent.isValid()); | |
1338 if (start != 0 || start != end) { | |
1339 m_sourceRowCache.clear(); | |
1340 reset(); | |
1341 return; | |
1342 } | |
1343 | |
1344 m_sourceRowCache.clear(); | |
1345 QModelIndex treeIndex = mapFromSource(sourceModel().index(start, 0)); | |
1346 QModelIndex treeParent = treeIndex.parent(); | |
1347 if (rowCount(treeParent) == 1) { | |
1348 beginInsertRows(QModelIndex(), 0, 0); | |
1349 endInsertRows(); | |
1350 } else { | |
1351 beginInsertRows(treeParent, treeIndex.row(), treeIndex.row()); | |
1352 endInsertRows(); | |
1353 } | |
1354 } | |
1355 | |
1356 | |
1357 void sourceRowsRemoved(QModelIndex &parent, int start, int end); | |
1358 { | |
1359 Q_UNUSED(parent); // Avoid warnings when compiling release | |
1360 Q_ASSERT(!parent.isValid()); | |
1361 if (m_sourceRowCache.isEmpty()) | |
1362 return; | |
1363 for (int i = end; i >= start;) { | |
1364 QList<int>::iterator it; | |
1365 it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), i); | |
1366 // playing it safe | |
1367 if (it == m_sourceRowCache.end()) { | |
1368 m_sourceRowCache.clear(); | |
1369 reset(); | |
1370 return; | |
1371 } | |
1372 | |
1373 if (*it != i) | |
1374 --it; | |
1375 int row = qMax(0, it - m_sourceRowCache.begin()); | |
1376 int offset = m_sourceRowCache[row]; | |
1377 QModelIndex dateParent = index(row, 0); | |
1378 // If we can remove all the rows in the date do that and skip over them | |
1379 int rc = rowCount(dateParent); | |
1380 if (i - rc + 1 == offset && start <= i - rc + 1) { | |
1381 beginRemoveRows(QModelIndex(), row, row); | |
1382 m_sourceRowCache.removeAt(row); | |
1383 i -= rc + 1; | |
1384 } else { | |
1385 beginRemoveRows(dateParent, i - offset, i - offset); | |
1386 ++row; | |
1387 --i; | |
1388 } | |
1389 for (int j = row; j < m_sourceRowCache.count(); ++j) | |
1390 --m_sourceRowCache[j]; | |
1391 endRemoveRows(); | |
1392 } | |
1393 } | |
1394 | |
1395 private: | |
1396 | |
1397 // Translate the top level date row into the offset where that date starts | |
1398 int sourceDateRow(int row) | |
1399 { | |
1400 if (row <= 0) | |
1401 return 0; | |
1402 | |
1403 if (m_sourceRowCache.isEmpty()) | |
1404 rowCount(QModelIndex()); | |
1405 | |
1406 if (row >= m_sourceRowCache.count()) { | |
1407 if (!sourceModel()) | |
1408 return 0; | |
1409 return sourceModel().rowCount(); | |
1410 } | |
1411 return m_sourceRowCache.at(row); | |
1412 } | |
1413 | |
1414 mutable QList<int> m_sourceRowCache; | |
1415 } | |
1416 | |
1417 // A modified QSortFilterProxyModel that always accepts the root nodes in the tree | |
1418 // so filtering is only done on the children. | |
1419 // Used in the HistoryDialog | |
1420 class TreeProxyModel : public QSortFilterProxyModel | |
1421 { | |
1422 Q_OBJECT | |
1423 | |
1424 public: | |
1425 this(QObject *parent = null) | |
1426 { | |
1427 super(parent); | |
1428 setSortRole(HistoryModel.DateTimeRole); | |
1429 setFilterCaseSensitivity(Qt.CaseInsensitive); | |
1430 } | |
1431 | |
1432 protected: | |
1433 bool filterAcceptsRow(int source_row, QModelIndex &source_parent) | |
1434 { | |
1435 if (!source_parent.isValid()) | |
1436 return true; | |
1437 return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); | |
1438 } | |
1439 } | |
1440 | |
1441 #include "ui_history.h" | |
1442 | |
1443 class HistoryDialog : public QDialog, public Ui_HistoryDialog | |
1444 { | |
1445 Q_OBJECT | |
1446 | |
1447 signals: | |
1448 void openUrl(QUrl &url); | |
1449 | |
1450 public: | |
1451 | |
1452 this(QWidget *parent = null, HistoryManager *history = null) : QDialog(parent) | |
1453 { | |
1454 HistoryManager *history = setHistory; | |
1455 if (!history) | |
1456 history = BrowserApplication::historyManager(); | |
1457 setupUi(this); | |
1458 tree.setUniformRowHeights(true); | |
1459 tree.setSelectionBehavior(QAbstractItemView::SelectRows); | |
1460 tree.setTextElideMode(Qt.ElideMiddle); | |
1461 QAbstractItemModel *model = history.historyTreeModel(); | |
1462 TreeProxyModel *proxyModel = new TreeProxyModel(this); | |
1463 connect(search, SIGNAL(textChanged(QString)), | |
1464 proxyModel, SLOT(setFilterFixedString(QString))); | |
1465 connect(removeButton, SIGNAL(clicked()), tree, SLOT(removeOne())); | |
1466 connect(removeAllButton, SIGNAL(clicked()), history, SLOT(clear())); | |
1467 proxyModel.setSourceModel(model); | |
1468 tree.setModel(proxyModel); | |
1469 tree.setExpanded(proxyModel.index(0, 0), true); | |
1470 tree.setAlternatingRowColors(true); | |
1471 QFontMetrics fm(font()); | |
1472 int header = fm.width(QLatin1Char('m')) * 40; | |
1473 tree.header().resizeSection(0, header); | |
1474 tree.header().setStretchLastSection(true); | |
1475 connect(tree, SIGNAL(activated(QModelIndex&)), | |
1476 this, SLOT(open())); | |
1477 tree.setContextMenuPolicy(Qt.CustomContextMenu); | |
1478 connect(tree, SIGNAL(customContextMenuRequested(QPoint &)), | |
1479 this, SLOT(customContextMenuRequested(QPoint &))); | |
1480 } | |
1481 | |
1482 private slots: | |
1483 | |
1484 void customContextMenuRequested(QPoint &pos) | |
1485 { | |
1486 QMenu menu; | |
1487 QModelIndex index = tree.indexAt(pos); | |
1488 index = index.sibling(index.row(), 0); | |
1489 if (index.isValid() && !tree.model().hasChildren(index)) { | |
1490 menu.addAction(tr("Open"), this, SLOT(open())); | |
1491 menu.addSeparator(); | |
1492 menu.addAction(tr("Copy"), this, SLOT(copy())); | |
1493 } | |
1494 menu.addAction(tr("Delete"), tree, SLOT(removeOne())); | |
1495 menu.exec(QCursor::pos()); | |
1496 } | |
1497 | |
1498 | |
1499 void open() | |
1500 { | |
1501 QModelIndex index = tree.currentIndex(); | |
1502 if (!index.parent().isValid()) | |
1503 return; | |
1504 emit openUrl(index.data(HistoryModel.UrlRole).toUrl()); | |
1505 } | |
1506 | |
1507 void copy() | |
1508 { | |
1509 QModelIndex index = tree.currentIndex(); | |
1510 if (!index.parent().isValid()) | |
1511 return; | |
1512 QString url = index.data(HistoryModel.UrlStringRole).toString(); | |
1513 | |
1514 QClipboard *clipboard = QApplication::clipboard(); | |
1515 clipboard.setText(url); | |
1516 } | |
1517 } |