diff -r f99d60d6b59b -r 2602857055c5 eric6/WebBrowser/History/HistoryMenu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/WebBrowser/History/HistoryMenu.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2019 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=None): + """ + Public method to get the number of columns. + + @param parent index of parent (QModelIndex) + @return number of columns (integer) + """ + if parent is None: + parent = QModelIndex() + + return self.__treeModel.columnCount(self.mapToSource(parent)) + + def rowCount(self, parent=None): + """ + Public method to determine the number of rows. + + @param parent index of parent (QModelIndex) + @return number of rows (integer) + """ + if parent is None: + parent = QModelIndex() + + 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=None): + """ + 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 parent is None: + parent = 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 newTab(QUrl, str) emitted to open a URL in a new tab + @signal newBackgroundTab(QUrl, str) emitted to open a URL in a new + background tab + @signal newWindow(QUrl, str) emitted to open a URL in a new window + @signal newPrivateWindow(QUrl, str) emitted to open a URL in a new + private window + """ + openUrl = pyqtSignal(QUrl, str) + newTab = pyqtSignal(QUrl, str) + newBackgroundTab = pyqtSignal(QUrl, str) + newWindow = pyqtSignal(QUrl, str) + newPrivateWindow = 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.__mw = parent + + self.__historyManager = None + self.__historyMenuModel = None + self.__initialActions = [] + self.__mostVisitedMenu = None + + 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.newTab.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + elif self._keyboardModifiers & Qt.ShiftModifier: + self.newWindow.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: + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__historyManager = 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.newTab.connect(self.newTab) + self.__mostVisitedMenu.newBackgroundTab.connect( + self.newBackgroundTab) + self.__mostVisitedMenu.newWindow.connect(self.newWindow) + self.__mostVisitedMenu.newPrivateWindow.connect( + self.newPrivateWindow) + self.addMenu(self.__mostVisitedMenu) + 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): + """ + Public slot to show the history dialog. + """ + from .HistoryDialog import HistoryDialog + dlg = HistoryDialog(self.__mw) + dlg.openUrl.connect(self.openUrl) + dlg.newTab.connect(self.newTab) + dlg.newBackgroundTab.connect(self.newBackgroundTab) + dlg.newWindow.connect(self.newWindow) + dlg.newPrivateWindow.connect(self.newPrivateWindow) + 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() + self.__tabWidget.clearClosedTabsList() + + 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) + act = self.__closedTabsMenu.addAction( + WebBrowser.WebBrowserWindow.WebBrowserWindow.icon(tab.url), + title) + act.setData(index) + act.triggered.connect( + lambda: self.__tabWidget.restoreClosedTab(act)) + 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 newTab(QUrl, str) emitted to open a URL in a new tab + @signal newBackgroundTab(QUrl, str) emitted to open a URL in a new + background tab + @signal newWindow(QUrl, str) emitted to open a URL in a new window + @signal newPrivateWindow(QUrl, str) emitted to open a URL in a new + private window + """ + openUrl = pyqtSignal(QUrl, str) + newTab = pyqtSignal(QUrl, str) + newBackgroundTab = pyqtSignal(QUrl, str) + newWindow = pyqtSignal(QUrl, str) + newPrivateWindow = 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.newTab.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + elif self._keyboardModifiers & Qt.ShiftModifier: + self.newWindow.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: + from WebBrowser.WebBrowserWindow import WebBrowserWindow + historyManager = WebBrowserWindow.historyManager() + self.__historyMenuModel = HistoryMostVisitedMenuModel( + historyManager.historyFilterModel(), self) + self.setModel(self.__historyMenuModel) + self.__historyMenuModel.sort(0) + + return False