Continued porting the web browser. QtWebEngine

Sat, 13 Feb 2016 13:36:01 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 13 Feb 2016 13:36:01 +0100
branch
QtWebEngine
changeset 4734
ce0b1f024da9
parent 4733
ae291a307ea6
child 4735
84e78ee0f361

Continued porting the web browser.

- added the history stuff

Preferences/__init__.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryCompleter.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryDialog.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryDialog.ui file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryFilterModel.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryManager.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryMenu.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryModel.py file | annotate | diff | comparison | revisions
WebBrowser/History/HistoryTreeModel.py file | annotate | diff | comparison | revisions
WebBrowser/History/__init__.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserView.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserWindow.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
--- a/Preferences/__init__.py	Fri Feb 12 19:12:03 2016 +0100
+++ b/Preferences/__init__.py	Sat Feb 13 13:36:01 2016 +0100
@@ -1016,6 +1016,7 @@
         "DefaultScheme": "https://",
         "UserStyleSheet": "",
         "ZoomValuesDB": "{}",       # empty JSON dictionary
+        "HistoryLimit": 30,
     }
     
     @classmethod
@@ -2709,7 +2710,7 @@
 ##                 "SearchLanguage", "SyncType", "SyncFtpPort",
 ##                 "SyncFtpIdleTimeout", "SyncEncryptionKeyLength"]:
     elif key in ["StartupBehavior", "MinimumFontSize",
-                 "MinimumLogicalFontSize"]:
+                 "MinimumLogicalFontSize", "HistoryLimit"]:
         return int(prefClass.settings.value(
             "WebBrowser/" + key, prefClass.webBrowserDefaults[key]))
 ##    elif key in ["SingleHelpWindow", "SaveGeometry", "WebSearchSuggestions",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryCompleter.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,303 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a special completer for the history.
+"""
+
+from __future__ import unicode_literals
+
+from PyQt5.QtCore import Qt, QRegExp, QTimer, QSortFilterProxyModel
+from PyQt5.QtWidgets import QTableView, QAbstractItemView, QCompleter
+
+from .HistoryModel import HistoryModel
+from .HistoryFilterModel import HistoryFilterModel
+
+
+class HistoryCompletionView(QTableView):
+    """
+    Class implementing a special completer view for history based completions.
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        super(HistoryCompletionView, self).__init__(parent)
+        
+        self.horizontalHeader().hide()
+        self.verticalHeader().hide()
+        
+        self.setShowGrid(False)
+        
+        self.setSelectionBehavior(QAbstractItemView.SelectRows)
+        self.setSelectionMode(QAbstractItemView.SingleSelection)
+        self.setTextElideMode(Qt.ElideRight)
+        
+        metrics = self.fontMetrics()
+        self.verticalHeader().setDefaultSectionSize(metrics.height())
+    
+    def resizeEvent(self, evt):
+        """
+        Protected method handling resize events.
+        
+        @param evt reference to the resize event (QResizeEvent)
+        """
+        self.horizontalHeader().resizeSection(0, 0.65 * self.width())
+        self.horizontalHeader().setStretchLastSection(True)
+        
+        super(HistoryCompletionView, self).resizeEvent(evt)
+    
+    def sizeHintForRow(self, row):
+        """
+        Public method to give a size hint for rows.
+        
+        @param row row number (integer)
+        @return desired row height (integer)
+        """
+        metrics = self.fontMetrics()
+        return metrics.height()
+
+
+class HistoryCompletionModel(QSortFilterProxyModel):
+    """
+    Class implementing a special model for history based completions.
+    """
+    HistoryCompletionRole = HistoryFilterModel.MaxRole + 1
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryCompletionModel, self).__init__(parent)
+        
+        self.__searchString = ""
+        self.__searchMatcher = QRegExp(
+            "", Qt.CaseInsensitive, QRegExp.FixedString)
+        self.__wordMatcher = QRegExp("", Qt.CaseInsensitive)
+        self.__isValid = False
+        
+        self.setDynamicSortFilter(True)
+    
+    def data(self, index, role=Qt.DisplayRole):
+        """
+        Public method to get data from the model.
+        
+        @param index index of history entry to get data for (QModelIndex)
+        @param role data role (integer)
+        @return history entry data
+        """
+        # If the model is valid, tell QCompleter that everything we have
+        # filtered matches what the user typed; if not, nothing matches
+        if role == self.HistoryCompletionRole and index.isValid():
+            if self.isValid():
+                return "t"
+            else:
+                return "f"
+        
+        if role == Qt.DisplayRole:
+            if index.column() == 0:
+                role = HistoryModel.UrlStringRole
+            else:
+                role = HistoryModel.TitleRole
+        
+        return QSortFilterProxyModel.data(self, index, role)
+    
+    def searchString(self):
+        """
+        Public method to get the current search string.
+        
+        @return current search string (string)
+        """
+        return self.__searchString
+    
+    def setSearchString(self, string):
+        """
+        Public method to set the current search string.
+        
+        @param string new search string (string)
+        """
+        if string == self.__searchString:
+            return
+        
+        self.__searchString = string
+        self.__searchMatcher.setPattern(self.__searchString)
+        self.__wordMatcher.setPattern(
+            "\\b" + QRegExp.escape(self.__searchString))
+        self.invalidateFilter()
+    
+    def isValid(self):
+        """
+        Public method to check the model for validity.
+        
+        @return flag indicating a valid status (boolean)
+        """
+        return self.__isValid
+    
+    def setValid(self, valid):
+        """
+        Public method to set the model's validity.
+        
+        @param valid flag indicating the new valid status (boolean)
+        """
+        if valid == self.__isValid:
+            return
+        
+        self.__isValid = valid
+        
+        # tell the history completer that the model has changed
+        self.dataChanged.emit(self.index(0, 0), self.index(0,
+                              self.rowCount() - 1))
+    
+    def filterAcceptsRow(self, sourceRow, sourceParent):
+        """
+        Public method to determine, if the row is acceptable.
+        
+        @param sourceRow row number in the source model (integer)
+        @param sourceParent index of the source item (QModelIndex)
+        @return flag indicating acceptance (boolean)
+        """
+        # Do a case-insensitive substring match against both the url and title.
+        # It's already ensured, that the user doesn't accidentally use regexp
+        # metacharacters (s. setSearchString()).
+        idx = self.sourceModel().index(sourceRow, 0, sourceParent)
+        
+        url = self.sourceModel().data(idx, HistoryModel.UrlStringRole)
+        if self.__searchMatcher.indexIn(url) != -1:
+            return True
+        
+        title = self.sourceModel().data(idx, HistoryModel.TitleRole)
+        if self.__searchMatcher.indexIn(title) != -1:
+            return True
+        
+        return False
+    
+    def lessThan(self, left, right):
+        """
+        Public method used to sort the displayed items.
+        
+        It implements a special sorting function based on the history entry's
+        frequency giving a bonus to hits that match on a word boundary so that
+        e.g. "dot.python-projects.org" is a better result for typing "dot" than
+        "slashdot.org". However, it only looks for the string in the host name,
+        not the entire URL, since while it makes sense to e.g. give
+        "www.phoronix.com" a bonus for "ph", it does NOT make sense to give
+        "www.yadda.com/foo.php" the bonus.
+        
+        @param left index of left item (QModelIndex)
+        @param right index of right item (QModelIndex)
+        @return true, if left is less than right (boolean)
+        """
+        frequency_L = \
+            self.sourceModel().data(left, HistoryFilterModel.FrequencyRole)
+        url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host()
+        title_L = self.sourceModel().data(left, HistoryModel.TitleRole)
+        
+        if self.__wordMatcher.indexIn(url_L) != -1 or \
+           self.__wordMatcher.indexIn(title_L) != -1:
+            frequency_L *= 2
+        
+        frequency_R = \
+            self.sourceModel().data(right, HistoryFilterModel.FrequencyRole)
+        url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host()
+        title_R = self.sourceModel().data(right, HistoryModel.TitleRole)
+        
+        if self.__wordMatcher.indexIn(url_R) != -1 or \
+           self.__wordMatcher.indexIn(title_R) != -1:
+            frequency_R *= 2
+        
+        # Sort results in descending frequency-derived score.
+        return frequency_R < frequency_L
+
+
+class HistoryCompleter(QCompleter):
+    """
+    Class implementing a completer for the browser history.
+    """
+    def __init__(self, model, parent=None):
+        """
+        Constructor
+        
+        @param model reference to the model (QAbstractItemModel)
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryCompleter, self).__init__(model, parent)
+        
+        self.setPopup(HistoryCompletionView())
+        
+        # Completion should be against the faked role.
+        self.setCompletionRole(HistoryCompletionModel.HistoryCompletionRole)
+        
+        # Since the completion role is faked, advantage of the sorted-model
+        # optimizations in QCompleter can be taken.
+        self.setCaseSensitivity(Qt.CaseSensitive)
+        self.setModelSorting(QCompleter.CaseSensitivelySortedModel)
+        
+        self.__searchString = ""
+        self.__filterTimer = QTimer(self)
+        self.__filterTimer.setSingleShot(True)
+        self.__filterTimer.timeout.connect(self.__updateFilter)
+    
+    def pathFromIndex(self, idx):
+        """
+        Public method to get a path for a given index.
+        
+        @param idx reference to the index (QModelIndex)
+        @return the actual URL from the history (string)
+        """
+        return self.model().data(idx, HistoryModel.UrlStringRole)
+    
+    def splitPath(self, path):
+        """
+        Public method to split the given path into strings, that are used to
+        match at each level in the model.
+        
+        @param path path to be split (string)
+        @return list of path elements (list of strings)
+        """
+        if path == self.__searchString:
+            return ["t"]
+        
+        # Queue an update to the search string. Wait a bit, so that if the user
+        # is quickly typing, the completer doesn't try to complete until they
+        # pause.
+        if self.__filterTimer.isActive():
+            self.__filterTimer.stop()
+        self.__filterTimer.start(150)
+        
+        # If the previous search results are not a superset of the current
+        # search results, tell the model that it is not valid yet.
+        if not path.startswith(self.__searchString):
+            self.model().setValid(False)
+        
+        self.__searchString = path
+        
+        # The actual filtering is done by the HistoryCompletionModel. Just
+        # return a short dummy here so that QCompleter thinks everything
+        # matched.
+        return ["t"]
+    
+    def __updateFilter(self):
+        """
+        Private slot to update the search string.
+        """
+        completionModel = self.model()
+        
+        # Tell the HistoryCompletionModel about the new search string.
+        completionModel.setSearchString(self.__searchString)
+        
+        # Sort the model.
+        completionModel.sort(0)
+        
+        # Mark it valid.
+        completionModel.setValid(True)
+        
+        # Now update the QCompleter widget, but only if the user is still
+        # typing a URL.
+        if self.widget() is not None and self.widget().hasFocus():
+            self.complete()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryDialog.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to manage history.
+"""
+
+from __future__ import unicode_literals
+
+from PyQt5.QtCore import pyqtSignal, Qt, QUrl
+from PyQt5.QtGui import QFontMetrics, QCursor
+from PyQt5.QtWidgets import QDialog, QMenu, QApplication
+
+from E5Gui.E5TreeSortFilterProxyModel import E5TreeSortFilterProxyModel
+
+from .HistoryModel import HistoryModel
+
+from .Ui_HistoryDialog import Ui_HistoryDialog
+
+
+class HistoryDialog(QDialog, Ui_HistoryDialog):
+    """
+    Class implementing a dialog to manage history.
+    
+    @signal openUrl(QUrl, str) emitted to open a URL in the current tab
+    @signal newUrl(QUrl, str) emitted to open a URL in a new tab
+    """
+    openUrl = pyqtSignal(QUrl, str)
+    newUrl = pyqtSignal(QUrl, str)
+    
+    def __init__(self, parent=None, manager=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget
+        @param manager reference to the history manager object (HistoryManager)
+        """
+        super(HistoryDialog, self).__init__(parent)
+        self.setupUi(self)
+        self.setWindowFlags(Qt.Window)
+        
+        self.__historyManager = manager
+        if self.__historyManager is None:
+            import WebBrowser.WebBrowserWindow
+            self.__historyManager = \
+                WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager()
+        
+        self.__model = self.__historyManager.historyTreeModel()
+        self.__proxyModel = E5TreeSortFilterProxyModel(self)
+        self.__proxyModel.setSortRole(HistoryModel.DateTimeRole)
+        self.__proxyModel.setFilterKeyColumn(-1)
+        self.__proxyModel.setSourceModel(self.__model)
+        self.historyTree.setModel(self.__proxyModel)
+        self.historyTree.expandAll()
+        fm = QFontMetrics(self.font())
+        header = fm.width("m") * 40
+        self.historyTree.header().resizeSection(0, header)
+        self.historyTree.header().setStretchLastSection(True)
+        self.historyTree.setContextMenuPolicy(Qt.CustomContextMenu)
+        
+        self.historyTree.activated.connect(self.__activated)
+        self.historyTree.customContextMenuRequested.connect(
+            self.__customContextMenuRequested)
+        
+        self.searchEdit.textChanged.connect(
+            self.__proxyModel.setFilterFixedString)
+        self.removeButton.clicked.connect(self.historyTree.removeSelected)
+        self.removeAllButton.clicked.connect(self.__historyManager.clear)
+        
+        self.__proxyModel.modelReset.connect(self.__modelReset)
+    
+    def __modelReset(self):
+        """
+        Private slot handling a reset of the tree view's model.
+        """
+        self.historyTree.expandAll()
+    
+    def __customContextMenuRequested(self, pos):
+        """
+        Private slot to handle the context menu request for the bookmarks tree.
+        
+        @param pos position the context menu was requested (QPoint)
+        """
+        menu = QMenu()
+        idx = self.historyTree.indexAt(pos)
+        idx = idx.sibling(idx.row(), 0)
+        if idx.isValid() and not self.historyTree.model().hasChildren(idx):
+            menu.addAction(
+                self.tr("&Open"), self.__openHistoryInCurrentTab)
+            menu.addAction(
+                self.tr("Open in New &Tab"), self.__openHistoryInNewTab)
+            menu.addSeparator()
+            menu.addAction(self.tr("&Copy"), self.__copyHistory)
+        menu.addAction(self.tr("&Remove"), self.historyTree.removeSelected)
+        menu.exec_(QCursor.pos())
+    
+    def __activated(self, idx):
+        """
+        Private slot to handle the activation of an entry.
+        
+        @param idx reference to the entry index (QModelIndex)
+        """
+        self.__openHistory(
+            QApplication.keyboardModifiers() & Qt.ControlModifier)
+        
+    def __openHistoryInCurrentTab(self):
+        """
+        Private slot to open a history entry in the current browser tab.
+        """
+        self.__openHistory(False)
+    
+    def __openHistoryInNewTab(self):
+        """
+        Private slot to open a history entry in a new browser tab.
+        """
+        self.__openHistory(True)
+    
+    def __openHistory(self, newTab):
+        """
+        Private method to open a history entry.
+        
+        @param newTab flag indicating to open the history entry in a new tab
+            (boolean)
+        """
+        idx = self.historyTree.currentIndex()
+        if newTab:
+            self.newUrl.emit(
+                idx.data(HistoryModel.UrlRole),
+                idx.data(HistoryModel.TitleRole))
+        else:
+            self.openUrl.emit(
+                idx.data(HistoryModel.UrlRole),
+                idx.data(HistoryModel.TitleRole))
+    
+    def __copyHistory(self):
+        """
+        Private slot to copy a history entry's URL to the clipboard.
+        """
+        idx = self.historyTree.currentIndex()
+        if not idx.parent().isValid():
+            return
+        
+        url = idx.data(HistoryModel.UrlStringRole)
+        
+        clipboard = QApplication.clipboard()
+        clipboard.setText(url)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryDialog.ui	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>HistoryDialog</class>
+ <widget class="QDialog" name="HistoryDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>750</width>
+    <height>450</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Manage History</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <property name="spacing">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="E5ClearableLineEdit" name="searchEdit">
+         <property name="toolTip">
+          <string>Enter search term for history entries</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="E5TreeView" name="historyTree">
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::ExtendedSelection</enum>
+     </property>
+     <property name="textElideMode">
+      <enum>Qt::ElideMiddle</enum>
+     </property>
+     <property name="uniformRowHeights">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QPushButton" name="removeButton">
+       <property name="toolTip">
+        <string>Press to remove the selected entries</string>
+       </property>
+       <property name="text">
+        <string>&amp;Remove</string>
+       </property>
+       <property name="autoDefault">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="removeAllButton">
+       <property name="toolTip">
+        <string>Press to remove all entries</string>
+       </property>
+       <property name="text">
+        <string>Remove &amp;All</string>
+       </property>
+       <property name="autoDefault">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="spacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>E5ClearableLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>E5Gui/E5LineEdit.h</header>
+  </customwidget>
+  <customwidget>
+   <class>E5TreeView</class>
+   <extends>QTreeView</extends>
+   <header>E5Gui/E5TreeView.h</header>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>searchEdit</tabstop>
+  <tabstop>historyTree</tabstop>
+  <tabstop>removeButton</tabstop>
+  <tabstop>removeAllButton</tabstop>
+  <tabstop>buttonBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>HistoryDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>252</x>
+     <y>445</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>HistoryDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>320</x>
+     <y>445</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryFilterModel.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,375 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the history filter model.
+"""
+
+from __future__ import unicode_literals
+
+from PyQt5.QtCore import Qt, QDateTime, QModelIndex, QAbstractProxyModel
+
+from .HistoryModel import HistoryModel
+
+
+class HistoryData(object):
+    """
+    Class storing some history data.
+    """
+    def __init__(self, offset, frequency=0):
+        """
+        Constructor
+        
+        @param offset tail offset (integer)
+        @param frequency frequency (integer)
+        """
+        self.tailOffset = offset
+        self.frequency = frequency
+    
+    def __eq__(self, other):
+        """
+        Special method implementing equality.
+        
+        @param other reference to the object to check against (HistoryData)
+        @return flag indicating equality (boolean)
+        """
+        return self.tailOffset == other.tailOffset and \
+            (self.frequency == -1 or other.frequency == -1 or
+             self.frequency == other.frequency)
+    
+    def __lt__(self, other):
+        """
+        Special method determining less relation.
+        
+        Note: Like the actual history entries the index mapping is sorted in
+        reverse order by offset
+        
+        @param other reference to the history data object to compare against
+            (HistoryEntry)
+        @return flag indicating less (boolean)
+        """
+        return self.tailOffset > other.tailOffset
+
+
+class HistoryFilterModel(QAbstractProxyModel):
+    """
+    Class implementing the history filter model.
+    """
+    FrequencyRole = HistoryModel.MaxRole + 1
+    MaxRole = FrequencyRole
+    
+    def __init__(self, sourceModel, parent=None):
+        """
+        Constructor
+        
+        @param sourceModel reference to the source model (QAbstractItemModel)
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryFilterModel, self).__init__(parent)
+        
+        self.__loaded = False
+        self.__filteredRows = []
+        self.__historyDict = {}
+        self.__scaleTime = QDateTime()
+        
+        self.setSourceModel(sourceModel)
+    
+    def historyContains(self, url):
+        """
+        Public method to check the history for an entry.
+        
+        @param url URL to check for (string)
+        @return flag indicating success (boolean)
+        """
+        self.__load()
+        return url in self.__historyDict
+    
+    def historyLocation(self, url):
+        """
+        Public method to get the row number of an entry in the source model.
+        
+        @param url URL to check for (tring)
+        @return row number in the source model (integer)
+        """
+        self.__load()
+        if url not in self.__historyDict:
+            return 0
+        
+        return self.sourceModel().rowCount() - self.__historyDict[url]
+    
+    def data(self, index, role=Qt.DisplayRole):
+        """
+        Public method to get data from the model.
+        
+        @param index index of history entry to get data for (QModelIndex)
+        @param role data role (integer)
+        @return history entry data
+        """
+        if role == self.FrequencyRole and index.isValid():
+            return self.__filteredRows[index.row()].frequency
+        
+        return QAbstractProxyModel.data(self, index, role)
+    
+    def setSourceModel(self, sourceModel):
+        """
+        Public method to set the source model.
+        
+        @param sourceModel reference to the source model (QAbstractItemModel)
+        """
+        if self.sourceModel() is not None:
+            self.sourceModel().modelReset.disconnect(self.__sourceReset)
+            self.sourceModel().dataChanged.disconnect(self.__sourceDataChanged)
+            self.sourceModel().rowsInserted.disconnect(
+                self.__sourceRowsInserted)
+            self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved)
+        
+        super(HistoryFilterModel, self).setSourceModel(sourceModel)
+        
+        if self.sourceModel() is not None:
+            self.__loaded = False
+            self.sourceModel().modelReset.connect(self.__sourceReset)
+            self.sourceModel().dataChanged.connect(self.__sourceDataChanged)
+            self.sourceModel().rowsInserted.connect(self.__sourceRowsInserted)
+            self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved)
+    
+    def __sourceDataChanged(self, topLeft, bottomRight):
+        """
+        Private slot to handle the change of data of the source model.
+        
+        @param topLeft index of top left data element (QModelIndex)
+        @param bottomRight index of bottom right data element (QModelIndex)
+        """
+        self.dataChanged.emit(
+            self.mapFromSource(topLeft), self.mapFromSource(bottomRight))
+    
+    def headerData(self, section, orientation, role=Qt.DisplayRole):
+        """
+        Public method to get the header data.
+        
+        @param section section number (integer)
+        @param orientation header orientation (Qt.Orientation)
+        @param role data role (integer)
+        @return header data
+        """
+        return self.sourceModel().headerData(section, orientation, role)
+    
+    def recalculateFrequencies(self):
+        """
+        Public method to recalculate the frequencies.
+        """
+        self.__sourceReset()
+    
+    def __sourceReset(self):
+        """
+        Private slot to handle a reset of the source model.
+        """
+        self.beginResetModel()
+        self.__loaded = False
+        self.endResetModel()
+    
+    def rowCount(self, parent=QModelIndex()):
+        """
+        Public method to determine the number of rows.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of rows (integer)
+        """
+        self.__load()
+        if parent.isValid():
+            return 0
+        return len(self.__historyDict)
+    
+    def columnCount(self, parent=QModelIndex()):
+        """
+        Public method to get the number of columns.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of columns (integer)
+        """
+        return self.sourceModel().columnCount(self.mapToSource(parent))
+    
+    def mapToSource(self, proxyIndex):
+        """
+        Public method to map an index to the source model index.
+        
+        @param proxyIndex reference to a proxy model index (QModelIndex)
+        @return source model index (QModelIndex)
+        """
+        self.__load()
+        sourceRow = self.sourceModel().rowCount() - proxyIndex.internalId()
+        return self.sourceModel().index(sourceRow, proxyIndex.column())
+    
+    def mapFromSource(self, sourceIndex):
+        """
+        Public method to map an index to the proxy model index.
+        
+        @param sourceIndex reference to a source model index (QModelIndex)
+        @return proxy model index (QModelIndex)
+        """
+        self.__load()
+        url = sourceIndex.data(HistoryModel.UrlStringRole)
+        if url not in self.__historyDict:
+            return QModelIndex()
+        
+        sourceOffset = self.sourceModel().rowCount() - sourceIndex.row()
+        
+        try:
+            row = self.__filteredRows.index(HistoryData(sourceOffset, -1))
+        except ValueError:
+            return QModelIndex()
+        
+        return self.createIndex(row, sourceIndex.column(), sourceOffset)
+    
+    def index(self, row, column, parent=QModelIndex()):
+        """
+        Public method to create an index.
+        
+        @param row row number for the index (integer)
+        @param column column number for the index (integer)
+        @param parent index of the parent item (QModelIndex)
+        @return requested index (QModelIndex)
+        """
+        self.__load()
+        if row < 0 or row >= self.rowCount(parent) or \
+           column < 0 or column >= self.columnCount(parent):
+            return QModelIndex()
+        
+        return self.createIndex(row, column,
+                                self.__filteredRows[row].tailOffset)
+
+    def parent(self, index):
+        """
+        Public method to get the parent index.
+        
+        @param index index of item to get parent (QModelIndex)
+        @return index of parent (QModelIndex)
+        """
+        return QModelIndex()
+    
+    def __load(self):
+        """
+        Private method to load the model data.
+        """
+        if self.__loaded:
+            return
+        
+        self.__filteredRows = []
+        self.__historyDict = {}
+        self.__scaleTime = QDateTime.currentDateTime()
+        
+        for sourceRow in range(self.sourceModel().rowCount()):
+            idx = self.sourceModel().index(sourceRow, 0)
+            url = idx.data(HistoryModel.UrlStringRole)
+            if url not in self.__historyDict:
+                sourceOffset = self.sourceModel().rowCount() - sourceRow
+                self.__filteredRows.append(
+                    HistoryData(sourceOffset, self.__frequencyScore(idx)))
+                self.__historyDict[url] = sourceOffset
+            else:
+                # the url is known already, so just update the frequency score
+                row = self.__filteredRows.index(
+                    HistoryData(self.__historyDict[url], -1))
+                self.__filteredRows[row].frequency += \
+                    self.__frequencyScore(idx)
+        
+        self.__loaded = True
+    
+    def __sourceRowsInserted(self, parent, start, end):
+        """
+        Private slot to handle the insertion of data in the source model.
+        
+        @param parent reference to the parent index (QModelIndex)
+        @param start start row (integer)
+        @param end end row (integer)
+        """
+        if start == end and start == 0:
+            if not self.__loaded:
+                return
+            
+            idx = self.sourceModel().index(start, 0, parent)
+            url = idx.data(HistoryModel.UrlStringRole)
+            currentFrequency = 0
+            if url in self.__historyDict:
+                row = self.__filteredRows.index(
+                    HistoryData(self.__historyDict[url], -1))
+                currentFrequency = self.__filteredRows[row].frequency
+                self.beginRemoveRows(QModelIndex(), row, row)
+                del self.__filteredRows[row]
+                del self.__historyDict[url]
+                self.endRemoveRows()
+            
+            self.beginInsertRows(QModelIndex(), 0, 0)
+            self.__filteredRows.insert(
+                0, HistoryData(
+                    self.sourceModel().rowCount(),
+                    self.__frequencyScore(idx) + currentFrequency))
+            self.__historyDict[url] = self.sourceModel().rowCount()
+            self.endInsertRows()
+    
+    def __sourceRowsRemoved(self, parent, start, end):
+        """
+        Private slot to handle the removal of data in the source model.
+        
+        @param parent reference to the parent index (QModelIndex)
+        @param start start row (integer)
+        @param end end row (integer)
+        """
+        self.__sourceReset()
+    
+    def removeRows(self, row, count, parent=QModelIndex()):
+        """
+        Public method to remove entries from the model.
+        
+        @param row row of the first entry to remove (integer)
+        @param count number of entries to remove (integer)
+        @param parent index of the parent entry (QModelIndex)
+        @return flag indicating successful removal (boolean)
+        """
+        if row < 0 or \
+           count <= 0 or \
+           row + count > self.rowCount(parent) or \
+           parent.isValid():
+            return False
+        
+        lastRow = row + count - 1
+        self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved)
+        self.beginRemoveRows(parent, row, lastRow)
+        oldCount = self.rowCount()
+        start = self.sourceModel().rowCount() - \
+            self.__filteredRows[row].tailOffset
+        end = self.sourceModel().rowCount() - \
+            self.__filteredRows[lastRow].tailOffset
+        self.sourceModel().removeRows(start, end - start + 1)
+        self.endRemoveRows()
+        self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved)
+        self.__loaded = False
+        if oldCount - count != self.rowCount():
+            self.beginResetModel()
+            self.endResetModel()
+        return True
+    
+    def __frequencyScore(self, sourceIndex):
+        """
+        Private method to calculate the frequency score.
+        
+        @param sourceIndex index of the source model (QModelIndex)
+        @return frequency score (integer)
+        """
+        loadTime = \
+            self.sourceModel().data(sourceIndex, HistoryModel.DateTimeRole)
+        days = loadTime.daysTo(self.__scaleTime)
+        
+        if days <= 1:
+            return 100
+        elif days < 8:      # within the last week
+            return 90
+        elif days < 15:     # within the last two weeks
+            return 70
+        elif days < 31:     # within the last month
+            return 50
+        elif days < 91:     # within the last 3 months
+            return 30
+        else:
+            return 10
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryManager.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,533 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the history manager.
+"""
+
+from __future__ import unicode_literals
+
+import os
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFileInfo, QDateTime, QDate, \
+    QTime, QUrl, QTimer, QFile, QIODevice, QByteArray, QDataStream, \
+    QTemporaryFile, QObject
+
+from E5Gui import E5MessageBox
+
+from Utilities.AutoSaver import AutoSaver
+import Utilities
+import Preferences
+
+HISTORY_VERSION = 42
+
+
+class HistoryEntry(object):
+    """
+    Class implementing a history entry.
+    """
+    def __init__(self, url=None, dateTime=None, title=None):
+        """
+        Constructor
+        
+        @param url URL of the history entry (string)
+        @param dateTime date and time this entry was created (QDateTime)
+        @param title title string for the history entry (string)
+        """
+        self.url = url and url or ""
+        self.dateTime = dateTime and dateTime or QDateTime()
+        self.title = title and title or ""
+    
+    def __eq__(self, other):
+        """
+        Special method determining equality.
+        
+        @param other reference to the history entry to compare against
+            (HistoryEntry)
+        @return flag indicating equality (boolean)
+        """
+        return other.title == self.title and \
+            other.url == self.url and \
+            other.dateTime == self.dateTime
+    
+    def __lt__(self, other):
+        """
+        Special method determining less relation.
+        
+        Note: History is sorted in reverse order by date and time
+        
+        @param other reference to the history entry to compare against
+            (HistoryEntry)
+        @return flag indicating less (boolean)
+        """
+        return self.dateTime > other.dateTime
+    
+    def userTitle(self):
+        """
+        Public method to get the title of the history entry.
+        
+        @return title of the entry (string)
+        """
+        if not self.title:
+            page = QFileInfo(QUrl(self.url).path()).fileName()
+            if page:
+                return page
+            return self.url
+        return self.title
+
+
+# TODO: Enhancement: Only one entry per URL for latest visit and add no. of visits
+class HistoryManager(QObject):
+    """
+    Class implementing the history manager.
+    
+    @signal historyCleared() emitted after the history has been cleared
+    @signal historyReset() emitted after the history has been reset
+    @signal entryAdded(HistoryEntry) emitted after a history entry has been
+        added
+    @signal entryRemoved(HistoryEntry) emitted after a history entry has been
+        removed
+    @signal entryUpdated(int) emitted after a history entry has been updated
+    @signal historySaved() emitted after the history was saved
+    """
+    historyCleared = pyqtSignal()
+    historyReset = pyqtSignal()
+    entryAdded = pyqtSignal(HistoryEntry)
+    entryRemoved = pyqtSignal(HistoryEntry)
+    entryUpdated = pyqtSignal(int)
+    historySaved = pyqtSignal()
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryManager, self).__init__(parent)
+        
+        self.__saveTimer = AutoSaver(self, self.save)
+        self.__daysToExpire = Preferences.getWebBrowser("HistoryLimit")
+        self.__history = []
+        self.__lastSavedUrl = ""
+        
+        self.__expiredTimer = QTimer(self)
+        self.__expiredTimer.setSingleShot(True)
+        self.__expiredTimer.timeout.connect(self.__checkForExpired)
+        
+        self.__frequencyTimer = QTimer(self)
+        self.__frequencyTimer.setSingleShot(True)
+        self.__frequencyTimer.timeout.connect(self.__refreshFrequencies)
+        
+        self.entryAdded.connect(self.__saveTimer.changeOccurred)
+        self.entryRemoved.connect(self.__saveTimer.changeOccurred)
+        
+        self.__load()
+        
+        from .HistoryModel import HistoryModel
+        from .HistoryFilterModel import HistoryFilterModel
+        from .HistoryTreeModel import HistoryTreeModel
+        
+        self.__historyModel = HistoryModel(self, self)
+        self.__historyFilterModel = \
+            HistoryFilterModel(self.__historyModel, self)
+        self.__historyTreeModel = \
+            HistoryTreeModel(self.__historyFilterModel, self)
+        
+        self.__startFrequencyTimer()
+    
+    def close(self):
+        """
+        Public method to close the history manager.
+        """
+        # remove history items on application exit
+        if self.__daysToExpire == -2:
+            self.clear()
+        self.__saveTimer.saveIfNeccessary()
+    
+    def history(self):
+        """
+        Public method to return the history.
+        
+        @return reference to the list of history entries (list of HistoryEntry)
+        """
+        return self.__history[:]
+    
+    def setHistory(self, history, loadedAndSorted=False):
+        """
+        Public method to set a new history.
+        
+        @param history reference to the list of history entries to be set
+            (list of HistoryEntry)
+        @param loadedAndSorted flag indicating that the list is sorted
+            (boolean)
+        """
+        self.__history = history[:]
+        if not loadedAndSorted:
+            self.__history.sort()
+        
+        self.__checkForExpired()
+        
+        if loadedAndSorted:
+            try:
+                self.__lastSavedUrl = self.__history[0].url
+            except IndexError:
+                self.__lastSavedUrl = ""
+        else:
+            self.__lastSavedUrl = ""
+            self.__saveTimer.changeOccurred()
+        self.historyReset.emit()
+    
+##    def _addHistoryEntry(self, itm):
+##        """
+##        Protected method to add a history item.
+##        
+##        @param itm reference to the history item to add (HistoryEntry)
+##        """
+##        import WebBrowser.WebBrowserWindow
+##        if WebBrowser.WebBrowserWindow.WebBrowserWindow\
+##                .mainWindow().getWindow().isPrivate():
+##            return
+##        
+##        self.__history.insert(0, itm)
+##        self.entryAdded.emit(itm)
+##        if len(self.__history) == 1:
+##            self.__checkForExpired()
+##    
+##    def _removeHistoryEntry(self, itm):
+##        """
+##        Protected method to remove a history item.
+##        
+##        @param itm reference to the history item to remove (HistoryEntry)
+##        """
+##        self.__lastSavedUrl = ""
+##        self.__history.remove(itm)
+##        self.entryRemoved.emit(itm)
+    
+    def addHistoryEntry(self, view):
+        """
+        Public method to add a history entry.
+        
+        @param view reference to the view to add an entry for
+        @type WebBrowserView
+        """
+        import WebBrowser.WebBrowserWindow
+        if WebBrowser.WebBrowserWindow.WebBrowserWindow\
+                .mainWindow().getWindow().isPrivate():
+            return
+        
+        url = view.url()
+        title = view.title()
+        
+        if url.scheme() not in ["eric", "about", "data"]:
+            if url.password():
+                # don't save the password in the history
+                url.setPassword("")
+            if url.host():
+                url.setHost(url.host().lower())
+            itm = HistoryEntry(url.toString(),
+                               QDateTime.currentDateTime(),
+                               title)
+            self.__history.insert(0, itm)
+            self.entryAdded.emit(itm)
+            if len(self.__history) == 1:
+                self.__checkForExpired()
+    
+    def updateHistoryEntry(self, url, title):
+        """
+        Public method to update a history entry.
+        
+        @param url URL of the entry to update (string)
+        @param title title of the entry to update (string)
+        """
+        cleanurl = QUrl(url)
+        if cleanurl.scheme() not in ["eric", "about"]:
+            for index in range(len(self.__history)):
+                if url == self.__history[index].url:
+                    self.__history[index].title = title
+                    self.__saveTimer.changeOccurred()
+                    if not self.__lastSavedUrl:
+                        self.__lastSavedUrl = self.__history[index].url
+                    self.entryUpdated.emit(index)
+                    break
+    
+    def removeHistoryEntry(self, url, title=""):
+        """
+        Public method to remove a history entry.
+        
+        @param url URL of the entry to remove (QUrl)
+        @param title title of the entry to remove (string)
+        """
+        for index in range(len(self.__history)):
+            if url == QUrl(self.__history[index].url) and \
+               (not title or title == self.__history[index].title):
+                itm = self.__history[index]
+                self.__lastSavedUrl = ""
+                self.__history.remove(itm)
+                self.entryRemoved.emit(itm)
+                break
+    
+    def historyModel(self):
+        """
+        Public method to get a reference to the history model.
+        
+        @return reference to the history model (HistoryModel)
+        """
+        return self.__historyModel
+    
+    def historyFilterModel(self):
+        """
+        Public method to get a reference to the history filter model.
+        
+        @return reference to the history filter model (HistoryFilterModel)
+        """
+        return self.__historyFilterModel
+    
+    def historyTreeModel(self):
+        """
+        Public method to get a reference to the history tree model.
+        
+        @return reference to the history tree model (HistoryTreeModel)
+        """
+        return self.__historyTreeModel
+    
+    def __checkForExpired(self):
+        """
+        Private slot to check entries for expiration.
+        """
+        if self.__daysToExpire < 0 or len(self.__history) == 0:
+            return
+        
+        now = QDateTime.currentDateTime()
+        nextTimeout = 0
+        
+        while self.__history:
+            checkForExpired = QDateTime(self.__history[-1].dateTime)
+            checkForExpired.setDate(
+                checkForExpired.date().addDays(self.__daysToExpire))
+            if now.daysTo(checkForExpired) > 7:
+                nextTimeout = 7 * 86400
+            else:
+                nextTimeout = now.secsTo(checkForExpired)
+            if nextTimeout > 0:
+                break
+            
+            itm = self.__history.pop(-1)
+            self.__lastSavedUrl = ""
+            self.entryRemoved.emit(itm)
+        self.__saveTimer.saveIfNeccessary()
+        
+        if nextTimeout > 0:
+            self.__expiredTimer.start(nextTimeout * 1000)
+    
+    def daysToExpire(self):
+        """
+        Public method to get the days for entry expiration.
+        
+        @return days for entry expiration (integer)
+        """
+        return self.__daysToExpire
+    
+    def setDaysToExpire(self, limit):
+        """
+        Public method to set the days for entry expiration.
+        
+        @param limit days for entry expiration (integer)
+        """
+        if self.__daysToExpire == limit:
+            return
+        
+        self.__daysToExpire = limit
+        self.__checkForExpired()
+        self.__saveTimer.changeOccurred()
+    
+    def preferencesChanged(self):
+        """
+        Public method to indicate a change of preferences.
+        """
+        self.setDaysToExpire(Preferences.getWebBrowser("HistoryLimit"))
+    
+    @pyqtSlot()
+    def clear(self, period=0):
+        """
+        Public slot to clear the complete history.
+        
+        @param period history period in milliseconds to be cleared (integer)
+        """
+        if period == 0:
+            self.__history = []
+            self.historyReset.emit()
+        else:
+            breakMS = QDateTime.currentMSecsSinceEpoch() - period
+            while self.__history and \
+                (QDateTime(self.__history[0].dateTime).toMSecsSinceEpoch() >
+                 breakMS):
+                itm = self.__history.pop(0)
+                self.entryRemoved.emit(itm)
+        self.__lastSavedUrl = ""
+        self.__saveTimer.changeOccurred()
+        self.__saveTimer.saveIfNeccessary()
+        self.historyCleared.emit()
+    
+    def getFileName(self):
+        """
+        Public method to get the file name of the history file.
+        
+        @return name of the history file (string)
+        """
+        return os.path.join(Utilities.getConfigDir(), "web_browser", "history")
+    
+    def reload(self):
+        """
+        Public method to reload the history.
+        """
+        self.__load()
+    
+    def __load(self):
+        """
+        Private method to load the saved history entries from disk.
+        """
+        historyFile = QFile(self.getFileName())
+        if not historyFile.exists():
+            return
+        if not historyFile.open(QIODevice.ReadOnly):
+            E5MessageBox.warning(
+                None,
+                self.tr("Loading History"),
+                self.tr(
+                    """<p>Unable to open history file <b>{0}</b>.<br/>"""
+                    """Reason: {1}</p>""")
+                .format(historyFile.fileName, historyFile.errorString()))
+            return
+        
+        history = []
+        
+        # double check, that the history file is sorted as it is read
+        needToSort = False
+        lastInsertedItem = HistoryEntry()
+        data = QByteArray(historyFile.readAll())
+        stream = QDataStream(data, QIODevice.ReadOnly)
+        stream.setVersion(QDataStream.Qt_4_6)
+        while not stream.atEnd():
+            ver = stream.readUInt32()
+            if ver != HISTORY_VERSION:
+                continue
+            itm = HistoryEntry()
+            itm.url = Utilities.readStringFromStream(stream)
+            stream >> itm.dateTime
+            itm.title = Utilities.readStringFromStream(stream)
+            
+            if not itm.dateTime.isValid():
+                continue
+            
+            if itm == lastInsertedItem:
+                if not lastInsertedItem.title and len(history) > 0:
+                    history[0].title = itm.title
+                continue
+            
+            if not needToSort and history and lastInsertedItem < itm:
+                needToSort = True
+            
+            history.insert(0, itm)
+            lastInsertedItem = itm
+        historyFile.close()
+        
+        if needToSort:
+            history.sort()
+        
+        self.setHistory(history, True)
+        
+        # if the history had to be sorted, rewrite the history sorted
+        if needToSort:
+            self.__lastSavedUrl = ""
+            self.__saveTimer.changeOccurred()
+    
+    def save(self):
+        """
+        Public slot to save the history entries to disk.
+        """
+        historyFile = QFile(self.getFileName())
+        if not historyFile.exists():
+            self.__lastSavedUrl = ""
+        
+        saveAll = self.__lastSavedUrl == ""
+        first = len(self.__history) - 1
+        if not saveAll:
+            # find the first one to save
+            for index in range(len(self.__history)):
+                if self.__history[index].url == self.__lastSavedUrl:
+                    first = index - 1
+                    break
+        if first == len(self.__history) - 1:
+            saveAll = True
+        
+        if saveAll:
+            # use a temporary file when saving everything
+            f = QTemporaryFile()
+            f.setAutoRemove(False)
+            opened = f.open()
+        else:
+            f = historyFile
+            opened = f.open(QIODevice.Append)
+        
+        if not opened:
+            E5MessageBox.warning(
+                None,
+                self.tr("Saving History"),
+                self.tr(
+                    """<p>Unable to open history file <b>{0}</b>.<br/>"""
+                    """Reason: {1}</p>""")
+                .format(f.fileName(), f.errorString()))
+            return
+        
+        for index in range(first, -1, -1):
+            data = QByteArray()
+            stream = QDataStream(data, QIODevice.WriteOnly)
+            stream.setVersion(QDataStream.Qt_4_6)
+            itm = self.__history[index]
+            stream.writeUInt32(HISTORY_VERSION)
+            stream.writeString(itm.url.encode("utf-8"))
+            stream << itm.dateTime
+            stream.writeString(itm.title.encode('utf-8'))
+            f.write(data)
+        
+        f.close()
+        if saveAll:
+            if historyFile.exists() and not historyFile.remove():
+                E5MessageBox.warning(
+                    None,
+                    self.tr("Saving History"),
+                    self.tr(
+                        """<p>Error removing old history file <b>{0}</b>."""
+                        """<br/>Reason: {1}</p>""")
+                    .format(historyFile.fileName(),
+                            historyFile.errorString()))
+            if not f.copy(historyFile.fileName()):
+                E5MessageBox.warning(
+                    None,
+                    self.tr("Saving History"),
+                    self.tr(
+                        """<p>Error moving new history file over old one """
+                        """(<b>{0}</b>).<br/>Reason: {1}</p>""")
+                    .format(historyFile.fileName(), f.errorString()))
+        self.historySaved.emit()
+        try:
+            self.__lastSavedUrl = self.__history[0].url
+        except IndexError:
+            self.__lastSavedUrl = ""
+    
+    def __refreshFrequencies(self):
+        """
+        Private slot to recalculate the refresh frequencies.
+        """
+        self.__historyFilterModel.recalculateFrequencies()
+        self.__startFrequencyTimer()
+    
+    def __startFrequencyTimer(self):
+        """
+        Private method to start the timer to recalculate the frequencies.
+        """
+        tomorrow = QDateTime(QDate.currentDate().addDays(1), QTime(3, 0))
+        self.__frequencyTimer.start(
+            QDateTime.currentDateTime().secsTo(tomorrow) * 1000)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryMenu.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,480 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the history menu.
+"""
+
+from __future__ import unicode_literals
+
+import sys
+
+from PyQt5.QtCore import pyqtSignal, Qt, QMimeData, QUrl, QModelIndex, \
+    QSortFilterProxyModel, QAbstractProxyModel
+from PyQt5.QtWidgets import QMenu
+
+from E5Gui.E5ModelMenu import E5ModelMenu
+from E5Gui import E5MessageBox
+
+from .HistoryModel import HistoryModel
+
+import UI.PixmapCache
+
+
+class HistoryMenuModel(QAbstractProxyModel):
+    """
+    Class implementing a model for the history menu.
+    
+    It maps the first bunch of items of the source model to the root.
+    """
+    MOVEDROWS = 15
+    
+    def __init__(self, sourceModel, parent=None):
+        """
+        Constructor
+        
+        @param sourceModel reference to the source model (QAbstractItemModel)
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryMenuModel, self).__init__(parent)
+        
+        self.__treeModel = sourceModel
+        
+        self.setSourceModel(sourceModel)
+    
+    def bumpedRows(self):
+        """
+        Public method to determine the number of rows moved to the root.
+        
+        @return number of rows moved to the root (integer)
+        """
+        first = self.__treeModel.index(0, 0)
+        if not first.isValid():
+            return 0
+        return min(self.__treeModel.rowCount(first), self.MOVEDROWS)
+    
+    def columnCount(self, parent=QModelIndex()):
+        """
+        Public method to get the number of columns.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of columns (integer)
+        """
+        return self.__treeModel.columnCount(self.mapToSource(parent))
+    
+    def rowCount(self, parent=QModelIndex()):
+        """
+        Public method to determine the number of rows.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of rows (integer)
+        """
+        if parent.column() > 0:
+            return 0
+        
+        if not parent.isValid():
+            folders = self.sourceModel().rowCount()
+            bumpedItems = self.bumpedRows()
+            if bumpedItems <= self.MOVEDROWS and \
+                    bumpedItems == self.sourceModel().rowCount(
+                        self.sourceModel().index(0, 0)):
+                folders -= 1
+            return bumpedItems + folders
+        
+        if parent.internalId() == sys.maxsize:
+            if parent.row() < self.bumpedRows():
+                return 0
+        
+        idx = self.mapToSource(parent)
+        defaultCount = self.sourceModel().rowCount(idx)
+        if idx == self.sourceModel().index(0, 0):
+            return defaultCount - self.bumpedRows()
+        
+        return defaultCount
+    
+    def mapFromSource(self, sourceIndex):
+        """
+        Public method to map an index to the proxy model index.
+        
+        @param sourceIndex reference to a source model index (QModelIndex)
+        @return proxy model index (QModelIndex)
+        """
+        sourceRow = self.__treeModel.mapToSource(sourceIndex).row()
+        return self.createIndex(
+            sourceIndex.row(), sourceIndex.column(), sourceRow)
+    
+    def mapToSource(self, proxyIndex):
+        """
+        Public method to map an index to the source model index.
+        
+        @param proxyIndex reference to a proxy model index (QModelIndex)
+        @return source model index (QModelIndex)
+        """
+        if not proxyIndex.isValid():
+            return QModelIndex()
+        
+        if proxyIndex.internalId() == sys.maxsize:
+            bumpedItems = self.bumpedRows()
+            if proxyIndex.row() < bumpedItems:
+                return self.__treeModel.index(
+                    proxyIndex.row(), proxyIndex.column(),
+                    self.__treeModel.index(0, 0))
+            if bumpedItems <= self.MOVEDROWS and \
+                    bumpedItems == self.sourceModel().rowCount(
+                        self.__treeModel.index(0, 0)):
+                bumpedItems -= 1
+            return self.__treeModel.index(proxyIndex.row() - bumpedItems,
+                                          proxyIndex.column())
+        
+        historyIndex = self.__treeModel.sourceModel()\
+            .index(proxyIndex.internalId(), proxyIndex.column())
+        treeIndex = self.__treeModel.mapFromSource(historyIndex)
+        return treeIndex
+    
+    def index(self, row, column, parent=QModelIndex()):
+        """
+        Public method to create an index.
+        
+        @param row row number for the index (integer)
+        @param column column number for the index (integer)
+        @param parent index of the parent item (QModelIndex)
+        @return requested index (QModelIndex)
+        """
+        if row < 0 or \
+           column < 0 or \
+           column >= self.columnCount(parent) or \
+           parent.column() > 0:
+            return QModelIndex()
+        
+        if not parent.isValid():
+            return self.createIndex(row, column, sys.maxsize)
+        
+        treeIndexParent = self.mapToSource(parent)
+        
+        bumpedItems = 0
+        if treeIndexParent == self.sourceModel().index(0, 0):
+            bumpedItems = self.bumpedRows()
+        treeIndex = self.__treeModel.index(
+            row + bumpedItems, column, treeIndexParent)
+        historyIndex = self.__treeModel.mapToSource(treeIndex)
+        historyRow = historyIndex.row()
+        if historyRow == -1:
+            historyRow = treeIndex.row()
+        return self.createIndex(row, column, historyRow)
+
+    def parent(self, index):
+        """
+        Public method to get the parent index.
+        
+        @param index index of item to get parent (QModelIndex)
+        @return index of parent (QModelIndex)
+        """
+        offset = index.internalId()
+        if offset == sys.maxsize or not index.isValid():
+            return QModelIndex()
+        
+        historyIndex = self.__treeModel.sourceModel().index(
+            index.internalId(), 0)
+        treeIndex = self.__treeModel.mapFromSource(historyIndex)
+        treeIndexParent = treeIndex.parent()
+        
+        sourceRow = self.sourceModel().mapToSource(treeIndexParent).row()
+        bumpedItems = self.bumpedRows()
+        if bumpedItems <= self.MOVEDROWS and \
+                bumpedItems == self.sourceModel().rowCount(
+                    self.sourceModel().index(0, 0)):
+            bumpedItems -= 1
+        
+        return self.createIndex(bumpedItems + treeIndexParent.row(),
+                                treeIndexParent.column(),
+                                sourceRow)
+    
+    def mimeData(self, indexes):
+        """
+        Public method to return the mime data.
+        
+        @param indexes list of indexes (QModelIndexList)
+        @return mime data (QMimeData)
+        """
+        urls = []
+        for index in indexes:
+            url = index.data(HistoryModel.UrlRole)
+            urls.append(url)
+        
+        mdata = QMimeData()
+        mdata.setUrls(urls)
+        return mdata
+
+
+class HistoryMostVisitedMenuModel(QSortFilterProxyModel):
+    """
+    Class implementing a model to show the most visited history entries.
+    """
+    def __init__(self, sourceModel, parent=None):
+        """
+        Constructor
+        
+        @param sourceModel reference to the source model (QAbstractItemModel)
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryMostVisitedMenuModel, self).__init__(parent)
+        
+        self.setDynamicSortFilter(True)
+        self.setSourceModel(sourceModel)
+    
+    def lessThan(self, left, right):
+        """
+        Public method used to sort the displayed items.
+        
+        @param left index of left item (QModelIndex)
+        @param right index of right item (QModelIndex)
+        @return true, if left is less than right (boolean)
+        """
+        from .HistoryFilterModel import HistoryFilterModel
+        frequency_L = \
+            self.sourceModel().data(left, HistoryFilterModel.FrequencyRole)
+        dateTime_L = \
+            self.sourceModel().data(left, HistoryModel.DateTimeRole)
+        frequency_R = \
+            self.sourceModel().data(right, HistoryFilterModel.FrequencyRole)
+        dateTime_R = \
+            self.sourceModel().data(right, HistoryModel.DateTimeRole)
+        
+        # Sort results in descending frequency-derived score. If frequencies
+        # are equal, sort on most recently viewed
+        if frequency_R == frequency_L:
+            return dateTime_R < dateTime_L
+        
+        return frequency_R < frequency_L
+
+
+class HistoryMenu(E5ModelMenu):
+    """
+    Class implementing the history menu.
+    
+    @signal openUrl(QUrl, str) emitted to open a URL in the current tab
+    @signal newUrl(QUrl, str) emitted to open a URL in a new tab
+    """
+    openUrl = pyqtSignal(QUrl, str)
+    newUrl = pyqtSignal(QUrl, str)
+    
+    def __init__(self, parent=None, tabWidget=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        @param tabWidget reference to the tab widget managing the browser
+            tabs (HelpTabWidget
+        """
+        E5ModelMenu.__init__(self, parent)
+        
+        self.__tabWidget = tabWidget
+        
+        self.__historyManager = None
+        self.__historyMenuModel = None
+        self.__initialActions = []
+        self.__mostVisitedMenu = None
+        
+        # TODO: Closed Tabs Manager
+##        self.__closedTabsMenu = QMenu(self.tr("Closed Tabs"))
+##        self.__closedTabsMenu.aboutToShow.connect(
+##            self.__aboutToShowClosedTabsMenu)
+##        self.__tabWidget.closedTabsManager().closedTabAvailable.connect(
+##            self.__closedTabAvailable)
+        
+        self.setMaxRows(7)
+        
+        self.activated.connect(self.__activated)
+        self.setStatusBarTextRole(HistoryModel.UrlStringRole)
+    
+    def __activated(self, idx):
+        """
+        Private slot handling the activated signal.
+        
+        @param idx index of the activated item (QModelIndex)
+        """
+        if self._keyboardModifiers & Qt.ControlModifier:
+            self.newUrl.emit(
+                idx.data(HistoryModel.UrlRole),
+                idx.data(HistoryModel.TitleRole))
+        else:
+            self.openUrl.emit(
+                idx.data(HistoryModel.UrlRole),
+                idx.data(HistoryModel.TitleRole))
+    
+    def prePopulated(self):
+        """
+        Public method to add any actions before the tree.
+       
+        @return flag indicating if any actions were added (boolean)
+        """
+        if self.__historyManager is None:
+            import WebBrowser.WebBrowserWindow
+            self.__historyManager = \
+                WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager()
+            self.__historyMenuModel = HistoryMenuModel(
+                self.__historyManager.historyTreeModel(), self)
+            self.setModel(self.__historyMenuModel)
+        
+        # initial actions
+        for act in self.__initialActions:
+            self.addAction(act)
+        if len(self.__initialActions) != 0:
+            self.addSeparator()
+        self.setFirstSeparator(self.__historyMenuModel.bumpedRows())
+        
+        return False
+    
+    def postPopulated(self):
+        """
+        Public method to add any actions after the tree.
+        """
+        if len(self.__historyManager.history()) > 0:
+            self.addSeparator()
+        
+        if self.__mostVisitedMenu is None:
+            self.__mostVisitedMenu = HistoryMostVisitedMenu(10, self)
+            self.__mostVisitedMenu.setTitle(self.tr("Most Visited"))
+            self.__mostVisitedMenu.openUrl.connect(self.openUrl)
+            self.__mostVisitedMenu.newUrl.connect(self.newUrl)
+        self.addMenu(self.__mostVisitedMenu)
+        # TODO: Closed Tabs Manager
+##        act = self.addMenu(self.__closedTabsMenu)
+##        act.setIcon(UI.PixmapCache.getIcon("trash.png"))
+##        act.setEnabled(self.__tabWidget.canRestoreClosedTab())
+        self.addSeparator()
+        
+        act = self.addAction(UI.PixmapCache.getIcon("history.png"),
+                             self.tr("Show All History..."))
+        act.triggered.connect(self.__showHistoryDialog)
+        act = self.addAction(UI.PixmapCache.getIcon("historyClear.png"),
+                             self.tr("Clear History..."))
+        act.triggered.connect(self.__clearHistoryDialog)
+    
+    def setInitialActions(self, actions):
+        """
+        Public method to set the list of actions that should appear first in
+        the menu.
+        
+        @param actions list of initial actions (list of QAction)
+        """
+        self.__initialActions = actions[:]
+        for act in self.__initialActions:
+            self.addAction(act)
+    
+    def __showHistoryDialog(self):
+        """
+        Private slot to show the history dialog.
+        """
+        from .HistoryDialog import HistoryDialog
+        dlg = HistoryDialog(self)
+        dlg.newUrl.connect(self.newUrl)
+        dlg.openUrl.connect(self.openUrl)
+        dlg.show()
+    
+    def __clearHistoryDialog(self):
+        """
+        Private slot to clear the history.
+        """
+        if self.__historyManager is not None and E5MessageBox.yesNo(
+                self,
+                self.tr("Clear History"),
+                self.tr("""Do you want to clear the history?""")):
+            self.__historyManager.clear()
+            # TODO: Closed Tabs Manager
+##            self.__tabWidget.clearClosedTabsList()
+    
+    # TODO: Closed Tabs Manager
+##    def __aboutToShowClosedTabsMenu(self):
+##        """
+##        Private slot to populate the closed tabs menu.
+##        """
+##        fm = self.__closedTabsMenu.fontMetrics()
+##        maxWidth = fm.width('m') * 40
+##        
+##        import WebBrowser.WebBrowserWindow
+##        self.__closedTabsMenu.clear()
+##        index = 0
+##        for tab in self.__tabWidget.closedTabsManager().allClosedTabs():
+##            title = fm.elidedText(tab.title, Qt.ElideRight, maxWidth)
+##            self.__closedTabsMenu.addAction(
+##                WebBrowser.WebBrowserWindow.WebBrowserWindow.icon(tab.url),
+##                title,
+##                self.__tabWidget.restoreClosedTab).setData(index)
+##            index += 1
+##        self.__closedTabsMenu.addSeparator()
+##        self.__closedTabsMenu.addAction(
+##            self.tr("Restore All Closed Tabs"),
+##            self.__tabWidget.restoreAllClosedTabs)
+##        self.__closedTabsMenu.addAction(
+##            self.tr("Clear List"),
+##            self.__tabWidget.clearClosedTabsList)
+##    
+##    def __closedTabAvailable(self, avail):
+##        """
+##        Private slot to handle changes of the availability of closed tabs.
+##        
+##        @param avail flag indicating the availability of closed tabs (boolean)
+##        """
+##        self.__closedTabsMenu.setEnabled(avail)
+
+
+class HistoryMostVisitedMenu(E5ModelMenu):
+    """
+    Class implementing the most visited history menu.
+    
+    @signal openUrl(QUrl, str) emitted to open a URL in the current tab
+    @signal newUrl(QUrl, str) emitted to open a URL in a new tab
+    """
+    openUrl = pyqtSignal(QUrl, str)
+    newUrl = pyqtSignal(QUrl, str)
+    
+    def __init__(self, count, parent=None):
+        """
+        Constructor
+        
+        @param count maximum number of entries to be shown (integer)
+        @param parent reference to the parent widget (QWidget)
+        """
+        E5ModelMenu.__init__(self, parent)
+        
+        self.__historyMenuModel = None
+        
+        self.setMaxRows(count + 1)
+        
+        self.activated.connect(self.__activated)
+        self.setStatusBarTextRole(HistoryModel.UrlStringRole)
+    
+    def __activated(self, idx):
+        """
+        Private slot handling the activated signal.
+        
+        @param idx index of the activated item (QModelIndex)
+        """
+        if self._keyboardModifiers & Qt.ControlModifier:
+            self.newUrl.emit(
+                idx.data(HistoryModel.UrlRole),
+                idx.data(HistoryModel.TitleRole))
+        else:
+            self.openUrl.emit(
+                idx.data(HistoryModel.UrlRole),
+                idx.data(HistoryModel.TitleRole))
+    
+    def prePopulated(self):
+        """
+        Public method to add any actions before the tree.
+       
+        @return flag indicating if any actions were added (boolean)
+        """
+        if self.__historyMenuModel is None:
+            import WebBrowser.WebBrowserWindow
+            historyManager = \
+                WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager()
+            self.__historyMenuModel = HistoryMostVisitedMenuModel(
+                historyManager.historyFilterModel(), self)
+            self.setModel(self.__historyMenuModel)
+        self.__historyMenuModel.sort(0)
+        
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryModel.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the history model.
+"""
+
+from __future__ import unicode_literals
+
+from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex, QUrl
+
+import WebBrowser.WebBrowserWindow
+
+
+class HistoryModel(QAbstractTableModel):
+    """
+    Class implementing the history model.
+    """
+    DateRole = Qt.UserRole + 1
+    DateTimeRole = Qt.UserRole + 2
+    UrlRole = Qt.UserRole + 3
+    UrlStringRole = Qt.UserRole + 4
+    TitleRole = Qt.UserRole + 5
+    MaxRole = TitleRole
+    
+    def __init__(self, historyManager, parent=None):
+        """
+        Constructor
+        
+        @param historyManager reference to the history manager object
+            (HistoryManager)
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryModel, self).__init__(parent)
+        
+        self.__historyManager = historyManager
+        
+        self.__headers = [
+            self.tr("Title"),
+            self.tr("Address"),
+        ]
+        
+        self.__historyManager.historyReset.connect(self.historyReset)
+        self.__historyManager.entryRemoved.connect(self.historyReset)
+        self.__historyManager.entryAdded.connect(self.entryAdded)
+        self.__historyManager.entryUpdated.connect(self.entryUpdated)
+    
+    def historyReset(self):
+        """
+        Public slot to reset the model.
+        """
+        self.beginResetModel()
+        self.endResetModel()
+    
+    def entryAdded(self):
+        """
+        Public slot to handle the addition of a history entry.
+        """
+        self.beginInsertRows(QModelIndex(), 0, 0)
+        self.endInsertRows()
+    
+    def entryUpdated(self, row):
+        """
+        Public slot to handle the update of a history entry.
+        
+        @param row row number of the updated entry (integer)
+        """
+        idx = self.index(row, 0)
+        self.dataChanged.emit(idx, idx)
+    
+    def headerData(self, section, orientation, role=Qt.DisplayRole):
+        """
+        Public method to get the header data.
+        
+        @param section section number (integer)
+        @param orientation header orientation (Qt.Orientation)
+        @param role data role (integer)
+        @return header data
+        """
+        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
+            try:
+                return self.__headers[section]
+            except IndexError:
+                pass
+        return QAbstractTableModel.headerData(self, section, orientation, role)
+    
+    def data(self, index, role=Qt.DisplayRole):
+        """
+        Public method to get data from the model.
+        
+        @param index index of history entry to get data for (QModelIndex)
+        @param role data role (integer)
+        @return history entry data
+        """
+        lst = self.__historyManager.history()
+        if index.row() < 0 or index.row() > len(lst):
+            return None
+        
+        itm = lst[index.row()]
+        if role == self.DateTimeRole:
+            return itm.dateTime
+        elif role == self.DateRole:
+            return itm.dateTime.date()
+        elif role == self.UrlRole:
+            return QUrl(itm.url)
+        elif role == self.UrlStringRole:
+            return itm.url
+        elif role == self.TitleRole:
+            return itm.userTitle()
+        elif role in [Qt.DisplayRole, Qt.EditRole]:
+            if index.column() == 0:
+                return itm.userTitle()
+            elif index.column() == 1:
+                return itm.url
+        elif role == Qt.DecorationRole:
+            if index.column() == 0:
+                return WebBrowser.WebBrowserWindow.WebBrowserWindow.icon(
+                    QUrl(itm.url))
+        
+        return None
+    
+    def columnCount(self, parent=QModelIndex()):
+        """
+        Public method to get the number of columns.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of columns (integer)
+        """
+        if parent.isValid():
+            return 0
+        else:
+            return len(self.__headers)
+    
+    def rowCount(self, parent=QModelIndex()):
+        """
+        Public method to determine the number of rows.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of rows (integer)
+        """
+        if parent.isValid():
+            return 0
+        else:
+            return len(self.__historyManager.history())
+    
+    def removeRows(self, row, count, parent=QModelIndex()):
+        """
+        Public method to remove history entries from the model.
+        
+        @param row row of the first history entry to remove (integer)
+        @param count number of history entries to remove (integer)
+        @param parent index of the parent entry (QModelIndex)
+        @return flag indicating successful removal (boolean)
+        """
+        if parent.isValid():
+            return False
+        
+        lastRow = row + count - 1
+        self.beginRemoveRows(parent, row, lastRow)
+        lst = self.__historyManager.history()[:]
+        for index in range(lastRow, row - 1, -1):
+            del lst[index]
+        self.__historyManager.historyReset.disconnect(self.historyReset)
+        self.__historyManager.setHistory(lst)
+        self.__historyManager.historyReset.connect(self.historyReset)
+        self.endRemoveRows()
+        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/HistoryTreeModel.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,377 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the history tree model.
+"""
+
+from __future__ import unicode_literals
+
+import bisect
+
+from PyQt5.QtCore import Qt, QModelIndex, QDate, QAbstractProxyModel
+
+from .HistoryModel import HistoryModel
+
+import UI.PixmapCache
+
+
+class HistoryTreeModel(QAbstractProxyModel):
+    """
+    Class implementing the history tree model.
+    """
+    def __init__(self, sourceModel, parent=None):
+        """
+        Constructor
+        
+        @param sourceModel reference to the source model (QAbstractItemModel)
+        @param parent reference to the parent object (QObject)
+        """
+        super(HistoryTreeModel, self).__init__(parent)
+        
+        self.__sourceRowCache = []
+        self.__removingDown = False
+        
+        self.setSourceModel(sourceModel)
+    
+    def headerData(self, section, orientation, role=Qt.DisplayRole):
+        """
+        Public method to get the header data.
+        
+        @param section section number (integer)
+        @param orientation header orientation (Qt.Orientation)
+        @param role data role (integer)
+        @return header data
+        """
+        return self.sourceModel().headerData(section, orientation, role)
+    
+    def data(self, index, role=Qt.DisplayRole):
+        """
+        Public method to get data from the model.
+        
+        @param index index of history entry to get data for (QModelIndex)
+        @param role data role (integer)
+        @return history entry data
+        """
+        if role in [Qt.DisplayRole, Qt.EditRole]:
+            start = index.internalId()
+            if start == 0:
+                offset = self.__sourceDateRow(index.row())
+                if index.column() == 0:
+                    idx = self.sourceModel().index(offset, 0)
+                    date = idx.data(HistoryModel.DateRole)
+                    if date == QDate.currentDate():
+                        return self.tr("Earlier Today")
+                    return date.toString("yyyy-MM-dd")
+                if index.column() == 1:
+                    return self.tr(
+                        "%n item(s)", "",
+                        self.rowCount(index.sibling(index.row(), 0)))
+        
+        elif role == Qt.DecorationRole:
+            if index.column() == 0 and not index.parent().isValid():
+                return UI.PixmapCache.getIcon("history.png")
+        
+        elif role == HistoryModel.DateRole:
+            if index.column() == 0 and index.internalId() == 0:
+                offset = self.__sourceDateRow(index.row())
+                idx = self.sourceModel().index(offset, 0)
+                return idx.data(HistoryModel.DateRole)
+        
+        return QAbstractProxyModel.data(self, index, role)
+    
+    def columnCount(self, parent=QModelIndex()):
+        """
+        Public method to get the number of columns.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of columns (integer)
+        """
+        return self.sourceModel().columnCount(self.mapToSource(parent))
+    
+    def rowCount(self, parent=QModelIndex()):
+        """
+        Public method to determine the number of rows.
+        
+        @param parent index of parent (QModelIndex)
+        @return number of rows (integer)
+        """
+        if parent.internalId() != 0 or \
+           parent.column() > 0 or \
+           self.sourceModel() is None:
+            return 0
+        
+        # row count OF dates
+        if not parent.isValid():
+            if self.__sourceRowCache:
+                return len(self.__sourceRowCache)
+            
+            currentDate = QDate()
+            rows = 0
+            totalRows = self.sourceModel().rowCount()
+            
+            for row in range(totalRows):
+                rowDate = self.sourceModel().index(row, 0)\
+                    .data(HistoryModel.DateRole)
+                if rowDate != currentDate:
+                    self.__sourceRowCache.append(row)
+                    currentDate = rowDate
+                    rows += 1
+            return rows
+        
+        # row count FOR a date
+        start = self.__sourceDateRow(parent.row())
+        end = self.__sourceDateRow(parent.row() + 1)
+        return end - start
+    
+    def __sourceDateRow(self, row):
+        """
+        Private method to translate the top level date row into the offset
+        where that date starts.
+        
+        @param row row number of the date (integer)
+        @return offset where that date starts (integer)
+        """
+        if row <= 0:
+            return 0
+        
+        if len(self.__sourceRowCache) == 0:
+            self.rowCount(QModelIndex())
+        
+        if row >= len(self.__sourceRowCache):
+            if self.sourceModel() is None:
+                return 0
+            return self.sourceModel().rowCount()
+        
+        return self.__sourceRowCache[row]
+    
+    def mapToSource(self, proxyIndex):
+        """
+        Public method to map an index to the source model index.
+        
+        @param proxyIndex reference to a proxy model index (QModelIndex)
+        @return source model index (QModelIndex)
+        """
+        offset = proxyIndex.internalId()
+        if offset == 0:
+            return QModelIndex()
+        startDateRow = self.__sourceDateRow(offset - 1)
+        return self.sourceModel().index(
+            startDateRow + proxyIndex.row(), proxyIndex.column())
+    
+    def index(self, row, column, parent=QModelIndex()):
+        """
+        Public method to create an index.
+        
+        @param row row number for the index (integer)
+        @param column column number for the index (integer)
+        @param parent index of the parent item (QModelIndex)
+        @return requested index (QModelIndex)
+        """
+        if row < 0 or \
+           column < 0 or \
+           column >= self.columnCount(parent) or \
+           parent.column() > 0:
+            return QModelIndex()
+        
+        if not parent.isValid():
+            return self.createIndex(row, column, 0)
+        return self.createIndex(row, column, parent.row() + 1)
+
+    def parent(self, index):
+        """
+        Public method to get the parent index.
+        
+        @param index index of item to get parent (QModelIndex)
+        @return index of parent (QModelIndex)
+        """
+        offset = index.internalId()
+        if offset == 0 or not index.isValid():
+            return QModelIndex()
+        return self.createIndex(offset - 1, 0, 0)
+    
+    def hasChildren(self, parent=QModelIndex()):
+        """
+        Public method to check, if an entry has some children.
+        
+        @param parent index of the entry to check (QModelIndex)
+        @return flag indicating the presence of children (boolean)
+        """
+        grandparent = parent.parent()
+        if not grandparent.isValid():
+            return True
+        return False
+    
+    def flags(self, index):
+        """
+        Public method to get the item flags.
+        
+        @param index index of the item (QModelIndex)
+        @return flags (Qt.ItemFlags)
+        """
+        if not index.isValid():
+            return Qt.ItemFlags(Qt.NoItemFlags)
+        return Qt.ItemFlags(
+            Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
+    
+    def setSourceModel(self, sourceModel):
+        """
+        Public method to set the source model.
+        
+        @param sourceModel reference to the source model (QAbstractItemModel)
+        """
+        if self.sourceModel() is not None:
+            self.sourceModel().modelReset.disconnect(self.__sourceReset)
+            self.sourceModel().layoutChanged.disconnect(self.__sourceReset)
+            self.sourceModel().rowsInserted.disconnect(
+                self.__sourceRowsInserted)
+            self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved)
+        
+        super(HistoryTreeModel, self).setSourceModel(sourceModel)
+        
+        if self.sourceModel() is not None:
+            self.__loaded = False
+            self.sourceModel().modelReset.connect(self.__sourceReset)
+            self.sourceModel().layoutChanged.connect(self.__sourceReset)
+            self.sourceModel().rowsInserted.connect(self.__sourceRowsInserted)
+            self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved)
+        
+        self.beginResetModel()
+        self.endResetModel()
+    
+    def __sourceReset(self):
+        """
+        Private slot to handle a reset of the source model.
+        """
+        self.beginResetModel()
+        self.__sourceRowCache = []
+        self.endResetModel()
+    
+    def __sourceRowsInserted(self, parent, start, end):
+        """
+        Private slot to handle the insertion of data in the source model.
+        
+        @param parent reference to the parent index (QModelIndex)
+        @param start start row (integer)
+        @param end end row (integer)
+        """
+        if not parent.isValid():
+            if start != 0 or start != end:
+                self.beginResetModel()
+                self.__sourceRowCache = []
+                self.endResetModel()
+                return
+            
+            self.__sourceRowCache = []
+            treeIndex = self.mapFromSource(self.sourceModel().index(start, 0))
+            treeParent = treeIndex.parent()
+            if self.rowCount(treeParent) == 1:
+                self.beginInsertRows(QModelIndex(), 0, 0)
+                self.endInsertRows()
+            else:
+                self.beginInsertRows(treeParent, treeIndex.row(),
+                                     treeIndex.row())
+                self.endInsertRows()
+    
+    def mapFromSource(self, sourceIndex):
+        """
+        Public method to map an index to the proxy model index.
+        
+        @param sourceIndex reference to a source model index (QModelIndex)
+        @return proxy model index (QModelIndex)
+        """
+        if not sourceIndex.isValid():
+            return QModelIndex()
+        
+        if len(self.__sourceRowCache) == 0:
+            self.rowCount(QModelIndex())
+        
+        try:
+            row = self.__sourceRowCache.index(sourceIndex.row())
+        except ValueError:
+            row = bisect.bisect_left(self.__sourceRowCache, sourceIndex.row())
+        if row == len(self.__sourceRowCache) or \
+           self.__sourceRowCache[row] != sourceIndex.row():
+            row -= 1
+        dateRow = max(0, row)
+        row = sourceIndex.row() - self.__sourceRowCache[dateRow]
+        return self.createIndex(row, sourceIndex.column(), dateRow + 1)
+    
+    def removeRows(self, row, count, parent=QModelIndex()):
+        """
+        Public method to remove entries from the model.
+        
+        @param row row of the first entry to remove (integer)
+        @param count number of entries to remove (integer)
+        @param parent index of the parent entry (QModelIndex)
+        @return flag indicating successful removal (boolean)
+        """
+        if row < 0 or \
+           count <= 0 or \
+           row + count > self.rowCount(parent):
+            return False
+        
+        self.__removingDown = True
+        if parent.isValid() and self.rowCount(parent) == count - row:
+            self.beginRemoveRows(QModelIndex(), parent.row(), parent.row())
+        else:
+            self.beginRemoveRows(parent, row, row + count - 1)
+        if parent.isValid():
+            # removing pages
+            offset = self.__sourceDateRow(parent.row())
+            return self.sourceModel().removeRows(offset + row, count)
+        else:
+            # removing whole dates
+            for i in range(row + count - 1, row - 1, -1):
+                dateParent = self.index(i, 0)
+                offset = self.__sourceDateRow(dateParent.row())
+                if not self.sourceModel().removeRows(
+                        offset, self.rowCount(dateParent)):
+                    return False
+        return True
+    
+    def __sourceRowsRemoved(self, parent, start, end):
+        """
+        Private slot to handle the removal of data in the source model.
+        
+        @param parent reference to the parent index (QModelIndex)
+        @param start start row (integer)
+        @param end end row (integer)
+        """
+        if not self.__removingDown:
+            self.beginResetModel()
+            self.__sourceRowCache = []
+            self.endResetModel()
+            return
+        
+        if not parent.isValid():
+            if self.__sourceRowCache:
+                i = end
+                while i >= start:
+                    try:
+                        ind = self.__sourceRowCache.index(i)
+                    except ValueError:
+                        ind = bisect.bisect_left(self.__sourceRowCache, i)
+                    if ind == len(self.__sourceRowCache) or \
+                       self.__sourceRowCache[ind] != i:
+                        ind -= 1
+                    row = max(0, ind)
+                    offset = self.__sourceRowCache[row]
+                    dateParent = self.index(row, 0)
+                    # If we can remove all the rows in the date do that
+                    # and skip over them.
+                    rc = self.rowCount(dateParent)
+                    if i - rc + 1 == offset and start <= i - rc + 1:
+                        del self.__sourceRowCache[row]
+                        i -= rc + 1
+                    else:
+                        row += 1
+                        i -= 1
+                    for j in range(row, len(self.__sourceRowCache)):
+                        self.__sourceRowCache[j] -= 1
+            
+            if self.__removingDown:
+                self.endRemoveRows()
+                self.__removingDown = False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/History/__init__.py	Sat Feb 13 13:36:01 2016 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the history system.
+"""
--- a/WebBrowser/WebBrowserView.py	Fri Feb 12 19:12:03 2016 +0100
+++ b/WebBrowser/WebBrowserView.py	Sat Feb 13 13:36:01 2016 +0100
@@ -1475,9 +1475,7 @@
         self.setZoomValue(zoomValue)
         
         if ok:
-            pass
-            # TODO: History
-##            self.__mw.historyManager().addHistoryEntry(self.url())
+            self.__mw.historyManager().addHistoryEntry(self)
             # TODO: AdBlock
 ##            self.__mw.adBlockManager().page().hideBlockedPageEntries(self.page())
             # TODO: Password Manager
--- a/WebBrowser/WebBrowserWindow.py	Fri Feb 12 19:12:03 2016 +0100
+++ b/WebBrowser/WebBrowserWindow.py	Sat Feb 13 13:36:01 2016 +0100
@@ -84,7 +84,7 @@
 ##    _cookieJar = None
 ##    _helpEngine = None
     _bookmarksManager = None
-##    _historyManager = None
+    _historyManager = None
 ##    _passwordManager = None
 ##    _adblockManager = None
 ##    _downloadManager = None
@@ -1827,14 +1827,13 @@
 ##            menu.addSeparator()
 ##            menu.addAction(self.syncTocAct)
         
-        # TODO: History
-##        from .History.HistoryMenu import HistoryMenu
-##        self.historyMenu = HistoryMenu(self, self.__tabWidget)
-##        self.historyMenu.setTearOffEnabled(True)
-##        self.historyMenu.setTitle(self.tr('H&istory'))
-##        self.historyMenu.openUrl.connect(self.openUrl)
-##        self.historyMenu.newUrl.connect(self.openUrlNewTab)
-##        mb.addMenu(self.historyMenu)
+        from .History.HistoryMenu import HistoryMenu
+        self.historyMenu = HistoryMenu(self, self.__tabWidget)
+        self.historyMenu.setTearOffEnabled(True)
+        self.historyMenu.setTitle(self.tr('H&istory'))
+        self.historyMenu.openUrl.connect(self.openUrl)
+        self.historyMenu.newUrl.connect(self.openUrlNewTab)
+        mb.addMenu(self.historyMenu)
         
         from .Bookmarks.BookmarksMenu import BookmarksMenuBarMenu
         self.bookmarksMenu = BookmarksMenuBarMenu(self)
@@ -2804,9 +2803,8 @@
         # TODO: NetworkManager
 ##        self.networkAccessManager().preferencesChanged()
 ##        
-        # TODO: History
-##        self.historyManager().preferencesChanged()
-##        
+        self.historyManager().preferencesChanged()
+        
         self.__tabWidget.preferencesChanged()
         
         # TODO: OpenSearch
@@ -3502,19 +3500,19 @@
 ##        self.newTab(None, (req, QNetworkAccessManager.GetOperation, b""))
         self.newTab(url)
     
-##    @classmethod
-##    def historyManager(cls):
-##        """
-##        Class method to get a reference to the history manager.
-##        
-##        @return reference to the history manager (HistoryManager)
-##        """
-##        if cls._historyManager is None:
-##            from .History.HistoryManager import HistoryManager
-##            cls._historyManager = HistoryManager()
-##        
-##        return cls._historyManager
-##        
+    @classmethod
+    def historyManager(cls):
+        """
+        Class method to get a reference to the history manager.
+        
+        @return reference to the history manager (HistoryManager)
+        """
+        if cls._historyManager is None:
+            from .History.HistoryManager import HistoryManager
+            cls._historyManager = HistoryManager()
+        
+        return cls._historyManager
+        
 ##    @classmethod
 ##    def passwordManager(cls):
 ##        """
--- a/eric6.e4p	Fri Feb 12 19:12:03 2016 +0100
+++ b/eric6.e4p	Sat Feb 13 13:36:01 2016 +0100
@@ -1288,6 +1288,14 @@
     <Source>WebBrowser/Bookmarks/XbelReader.py</Source>
     <Source>WebBrowser/Bookmarks/XbelWriter.py</Source>
     <Source>WebBrowser/Bookmarks/__init__.py</Source>
+    <Source>WebBrowser/History/HistoryCompleter.py</Source>
+    <Source>WebBrowser/History/HistoryDialog.py</Source>
+    <Source>WebBrowser/History/HistoryFilterModel.py</Source>
+    <Source>WebBrowser/History/HistoryManager.py</Source>
+    <Source>WebBrowser/History/HistoryMenu.py</Source>
+    <Source>WebBrowser/History/HistoryModel.py</Source>
+    <Source>WebBrowser/History/HistoryTreeModel.py</Source>
+    <Source>WebBrowser/History/__init__.py</Source>
     <Source>WebBrowser/JavaScript/AutoFillJsObject.py</Source>
     <Source>WebBrowser/JavaScript/ExternalJsObject.py</Source>
     <Source>WebBrowser/JavaScript/__init__.py</Source>
@@ -1717,6 +1725,7 @@
     <Form>WebBrowser/Bookmarks/BookmarkPropertiesDialog.ui</Form>
     <Form>WebBrowser/Bookmarks/BookmarksDialog.ui</Form>
     <Form>WebBrowser/Bookmarks/BookmarksImportDialog.ui</Form>
+    <Form>WebBrowser/History/HistoryDialog.ui</Form>
     <Form>WebBrowser/SearchWidget.ui</Form>
     <Form>WebBrowser/ZoomManager/ZoomValuesDialog.ui</Form>
   </Forms>

eric ide

mercurial