Sat, 13 Feb 2016 13:36:01 +0100
Continued porting the web browser.
- added the history stuff
--- 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>&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 &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>