--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Helpviewer/History/HistoryFilterModel.py Mon Dec 28 16:03:33 2009 +0000 @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the history filter model. +""" + +from PyQt4.QtCore import * +from PyQt4.QtGui import 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) + """ + QAbstractProxyModel.__init__(self, 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 (QVariant) + """ + if role == self.FrequencyRole and index.isValid(): + return QVariant(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.disconnect(self.sourceModel(), + SIGNAL("modelReset()"), + self.__sourceReset) + self.disconnect(self.sourceModel(), + SIGNAL("dataChanged(const QModelIndex&, const QModelIndex&)"), + self.__sourceDataChanged) + self.disconnect(self.sourceModel(), + SIGNAL("rowsInserted(const QModelIndex &, int, int)"), + self.__sourceRowsInserted) + self.disconnect(self.sourceModel(), + SIGNAL("rowsRemoved(const QModelIndex &, int, int)"), + self.__sourceRowsRemoved) + + QAbstractProxyModel.setSourceModel(self, sourceModel) + + if self.sourceModel() is not None: + self.__loaded = False + self.connect(self.sourceModel(), + SIGNAL("modelReset()"), + self.__sourceReset) + self.connect(self.sourceModel(), + SIGNAL("dataChanged(const QModelIndex&, const QModelIndex&)"), + self.__sourceDataChanged) + self.connect(self.sourceModel(), + SIGNAL("rowsInserted(const QModelIndex &, int, int)"), + self.__sourceRowsInserted) + self.connect(self.sourceModel(), + SIGNAL("rowsRemoved(const QModelIndex &, int, int)"), + 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.emit(SIGNAL("dataChanged(const QModelIndex&, const QModelIndex&)"), + 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 (QVariant) + """ + 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.__loaded = False + self.reset() + + 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 = unicode(sourceIndex.data(HistoryModel.UrlStringRole).toString()) + 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 = unicode(idx.data(HistoryModel.UrlStringRole).toString()) + 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).toString() + 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 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.disconnect(self.sourceModel(), + SIGNAL("rowsRemoved(const QModelIndex &, int, int)"), + 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.connect(self.sourceModel(), + SIGNAL("rowsRemoved(const QModelIndex &, int, int)"), + self.__sourceRowsRemoved) + self.__loaded = False + if oldCount - count != self.rowCount(): + self.reset() + 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).toDateTime() + 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