1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the history menu.
8"""
9
10import sys
11import functools
12
13from PyQt5.QtCore import (
14    pyqtSignal, Qt, QMimeData, QUrl, QModelIndex, QSortFilterProxyModel,
15    QAbstractProxyModel
16)
17from PyQt5.QtWidgets import QMenu
18
19from E5Gui.E5ModelMenu import E5ModelMenu
20from E5Gui import E5MessageBox
21
22from .HistoryModel import HistoryModel
23
24import UI.PixmapCache
25
26
27class HistoryMenuModel(QAbstractProxyModel):
28    """
29    Class implementing a model for the history menu.
30
31    It maps the first bunch of items of the source model to the root.
32    """
33    MOVEDROWS = 15
34
35    def __init__(self, sourceModel, parent=None):
36        """
37        Constructor
38
39        @param sourceModel reference to the source model (QAbstractItemModel)
40        @param parent reference to the parent object (QObject)
41        """
42        super().__init__(parent)
43
44        self.__treeModel = sourceModel
45
46        self.setSourceModel(sourceModel)
47
48    def bumpedRows(self):
49        """
50        Public method to determine the number of rows moved to the root.
51
52        @return number of rows moved to the root (integer)
53        """
54        first = self.__treeModel.index(0, 0)
55        if not first.isValid():
56            return 0
57        return min(self.__treeModel.rowCount(first), self.MOVEDROWS)
58
59    def columnCount(self, parent=None):
60        """
61        Public method to get the number of columns.
62
63        @param parent index of parent (QModelIndex)
64        @return number of columns (integer)
65        """
66        if parent is None:
67            parent = QModelIndex()
68
69        return self.__treeModel.columnCount(self.mapToSource(parent))
70
71    def rowCount(self, parent=None):
72        """
73        Public method to determine the number of rows.
74
75        @param parent index of parent (QModelIndex)
76        @return number of rows (integer)
77        """
78        if parent is None:
79            parent = QModelIndex()
80
81        if parent.column() > 0:
82            return 0
83
84        if not parent.isValid():
85            folders = self.sourceModel().rowCount()
86            bumpedItems = self.bumpedRows()
87            if (
88                bumpedItems <= self.MOVEDROWS and
89                bumpedItems == self.sourceModel().rowCount(
90                    self.sourceModel().index(0, 0))
91            ):
92                folders -= 1
93            return bumpedItems + folders
94
95        if (
96            parent.internalId() == sys.maxsize and
97            parent.row() < self.bumpedRows()
98        ):
99            return 0
100
101        idx = self.mapToSource(parent)
102        defaultCount = self.sourceModel().rowCount(idx)
103        if idx == self.sourceModel().index(0, 0):
104            return defaultCount - self.bumpedRows()
105
106        return defaultCount
107
108    def mapFromSource(self, sourceIndex):
109        """
110        Public method to map an index to the proxy model index.
111
112        @param sourceIndex reference to a source model index (QModelIndex)
113        @return proxy model index (QModelIndex)
114        """
115        sourceRow = self.__treeModel.mapToSource(sourceIndex).row()
116        return self.createIndex(
117            sourceIndex.row(), sourceIndex.column(), sourceRow)
118
119    def mapToSource(self, proxyIndex):
120        """
121        Public method to map an index to the source model index.
122
123        @param proxyIndex reference to a proxy model index (QModelIndex)
124        @return source model index (QModelIndex)
125        """
126        if not proxyIndex.isValid():
127            return QModelIndex()
128
129        if proxyIndex.internalId() == sys.maxsize:
130            bumpedItems = self.bumpedRows()
131            if proxyIndex.row() < bumpedItems:
132                return self.__treeModel.index(
133                    proxyIndex.row(), proxyIndex.column(),
134                    self.__treeModel.index(0, 0))
135            if (
136                bumpedItems <= self.MOVEDROWS and
137                bumpedItems == self.sourceModel().rowCount(
138                    self.__treeModel.index(0, 0))
139            ):
140                bumpedItems -= 1
141            return self.__treeModel.index(proxyIndex.row() - bumpedItems,
142                                          proxyIndex.column())
143
144        historyIndex = self.__treeModel.sourceModel().index(
145            proxyIndex.internalId(), proxyIndex.column())
146        treeIndex = self.__treeModel.mapFromSource(historyIndex)
147        return treeIndex
148
149    def index(self, row, column, parent=None):
150        """
151        Public method to create an index.
152
153        @param row row number for the index (integer)
154        @param column column number for the index (integer)
155        @param parent index of the parent item (QModelIndex)
156        @return requested index (QModelIndex)
157        """
158        if parent is None:
159            parent = QModelIndex()
160
161        if (
162            row < 0 or
163            column < 0 or
164            column >= self.columnCount(parent) or
165            parent.column() > 0
166        ):
167            return QModelIndex()
168
169        if not parent.isValid():
170            return self.createIndex(row, column, sys.maxsize)
171
172        treeIndexParent = self.mapToSource(parent)
173
174        bumpedItems = 0
175        if treeIndexParent == self.sourceModel().index(0, 0):
176            bumpedItems = self.bumpedRows()
177        treeIndex = self.__treeModel.index(
178            row + bumpedItems, column, treeIndexParent)
179        historyIndex = self.__treeModel.mapToSource(treeIndex)
180        historyRow = historyIndex.row()
181        if historyRow == -1:
182            historyRow = treeIndex.row()
183        return self.createIndex(row, column, historyRow)
184
185    def parent(self, index):
186        """
187        Public method to get the parent index.
188
189        @param index index of item to get parent (QModelIndex)
190        @return index of parent (QModelIndex)
191        """
192        offset = index.internalId()
193        if offset == sys.maxsize or not index.isValid():
194            return QModelIndex()
195
196        historyIndex = self.__treeModel.sourceModel().index(
197            index.internalId(), 0)
198        treeIndex = self.__treeModel.mapFromSource(historyIndex)
199        treeIndexParent = treeIndex.parent()
200
201        sourceRow = self.sourceModel().mapToSource(treeIndexParent).row()
202        bumpedItems = self.bumpedRows()
203        if (
204            bumpedItems <= self.MOVEDROWS and
205            bumpedItems == self.sourceModel().rowCount(
206                self.sourceModel().index(0, 0))
207        ):
208            bumpedItems -= 1
209
210        return self.createIndex(bumpedItems + treeIndexParent.row(),
211                                treeIndexParent.column(),
212                                sourceRow)
213
214    def mimeData(self, indexes):
215        """
216        Public method to return the mime data.
217
218        @param indexes list of indexes (QModelIndexList)
219        @return mime data (QMimeData)
220        """
221        urls = []
222        for index in indexes:
223            url = index.data(HistoryModel.UrlRole)
224            urls.append(url)
225
226        mdata = QMimeData()
227        mdata.setUrls(urls)
228        return mdata
229
230
231class HistoryMostVisitedMenuModel(QSortFilterProxyModel):
232    """
233    Class implementing a model to show the most visited history entries.
234    """
235    def __init__(self, sourceModel, parent=None):
236        """
237        Constructor
238
239        @param sourceModel reference to the source model (QAbstractItemModel)
240        @param parent reference to the parent object (QObject)
241        """
242        super().__init__(parent)
243
244        self.setDynamicSortFilter(True)
245        self.setSourceModel(sourceModel)
246
247    def lessThan(self, left, right):
248        """
249        Public method used to sort the displayed items.
250
251        @param left index of left item (QModelIndex)
252        @param right index of right item (QModelIndex)
253        @return true, if left is less than right (boolean)
254        """
255        from .HistoryFilterModel import HistoryFilterModel
256        frequency_L = self.sourceModel().data(
257            left, HistoryFilterModel.FrequencyRole)
258        dateTime_L = self.sourceModel().data(
259            left, HistoryModel.DateTimeRole)
260        frequency_R = self.sourceModel().data(
261            right, HistoryFilterModel.FrequencyRole)
262        dateTime_R = self.sourceModel().data(
263            right, HistoryModel.DateTimeRole)
264
265        # Sort results in descending frequency-derived score. If frequencies
266        # are equal, sort on most recently viewed
267        if frequency_R == frequency_L:
268            return dateTime_R < dateTime_L
269
270        return frequency_R < frequency_L
271
272
273class HistoryMenu(E5ModelMenu):
274    """
275    Class implementing the history menu.
276
277    @signal openUrl(QUrl, str) emitted to open a URL in the current tab
278    @signal newTab(QUrl, str) emitted to open a URL in a new tab
279    @signal newBackgroundTab(QUrl, str) emitted to open a URL in a new
280        background tab
281    @signal newWindow(QUrl, str) emitted to open a URL in a new window
282    @signal newPrivateWindow(QUrl, str) emitted to open a URL in a new
283        private window
284    """
285    openUrl = pyqtSignal(QUrl, str)
286    newTab = pyqtSignal(QUrl, str)
287    newBackgroundTab = pyqtSignal(QUrl, str)
288    newWindow = pyqtSignal(QUrl, str)
289    newPrivateWindow = pyqtSignal(QUrl, str)
290
291    def __init__(self, parent=None, tabWidget=None):
292        """
293        Constructor
294
295        @param parent reference to the parent widget (QWidget)
296        @param tabWidget reference to the tab widget managing the browser
297            tabs (HelpTabWidget
298        """
299        E5ModelMenu.__init__(self, parent)
300
301        self.__tabWidget = tabWidget
302        self.__mw = parent
303
304        self.__historyManager = None
305        self.__historyMenuModel = None
306        self.__initialActions = []
307        self.__mostVisitedMenu = None
308
309        self.__closedTabsMenu = QMenu(self.tr("Closed Tabs"))
310        self.__closedTabsMenu.aboutToShow.connect(
311            self.__aboutToShowClosedTabsMenu)
312        self.__tabWidget.closedTabsManager().closedTabAvailable.connect(
313            self.__closedTabAvailable)
314
315        self.setMaxRows(7)
316
317        self.activated.connect(self.__activated)
318        self.setStatusBarTextRole(HistoryModel.UrlStringRole)
319
320    def __activated(self, idx):
321        """
322        Private slot handling the activated signal.
323
324        @param idx index of the activated item (QModelIndex)
325        """
326        if self._keyboardModifiers & Qt.KeyboardModifier.ControlModifier:
327            self.newTab.emit(
328                idx.data(HistoryModel.UrlRole),
329                idx.data(HistoryModel.TitleRole))
330        elif self._keyboardModifiers & Qt.KeyboardModifier.ShiftModifier:
331            self.newWindow.emit(
332                idx.data(HistoryModel.UrlRole),
333                idx.data(HistoryModel.TitleRole))
334        else:
335            self.openUrl.emit(
336                idx.data(HistoryModel.UrlRole),
337                idx.data(HistoryModel.TitleRole))
338
339    def prePopulated(self):
340        """
341        Public method to add any actions before the tree.
342
343        @return flag indicating if any actions were added (boolean)
344        """
345        if self.__historyManager is None:
346            from WebBrowser.WebBrowserWindow import WebBrowserWindow
347            self.__historyManager = WebBrowserWindow.historyManager()
348            self.__historyMenuModel = HistoryMenuModel(
349                self.__historyManager.historyTreeModel(), self)
350            self.setModel(self.__historyMenuModel)
351
352        # initial actions
353        for act in self.__initialActions:
354            self.addAction(act)
355        if len(self.__initialActions) != 0:
356            self.addSeparator()
357        self.setFirstSeparator(self.__historyMenuModel.bumpedRows())
358
359        return False
360
361    def postPopulated(self):
362        """
363        Public method to add any actions after the tree.
364        """
365        if len(self.__historyManager.history()) > 0:
366            self.addSeparator()
367
368        if self.__mostVisitedMenu is None:
369            self.__mostVisitedMenu = HistoryMostVisitedMenu(10, self)
370            self.__mostVisitedMenu.setTitle(self.tr("Most Visited"))
371            self.__mostVisitedMenu.openUrl.connect(self.openUrl)
372            self.__mostVisitedMenu.newTab.connect(self.newTab)
373            self.__mostVisitedMenu.newBackgroundTab.connect(
374                self.newBackgroundTab)
375            self.__mostVisitedMenu.newWindow.connect(self.newWindow)
376            self.__mostVisitedMenu.newPrivateWindow.connect(
377                self.newPrivateWindow)
378        self.addMenu(self.__mostVisitedMenu)
379        act = self.addMenu(self.__closedTabsMenu)
380        act.setIcon(UI.PixmapCache.getIcon("trash"))
381        act.setEnabled(self.__tabWidget.canRestoreClosedTab())
382        self.addSeparator()
383
384        act = self.addAction(UI.PixmapCache.getIcon("history"),
385                             self.tr("Show All History..."))
386        act.triggered.connect(self.showHistoryDialog)
387        act = self.addAction(UI.PixmapCache.getIcon("historyClear"),
388                             self.tr("Clear History..."))
389        act.triggered.connect(self.__clearHistoryDialog)
390
391    def setInitialActions(self, actions):
392        """
393        Public method to set the list of actions that should appear first in
394        the menu.
395
396        @param actions list of initial actions (list of QAction)
397        """
398        self.__initialActions = actions[:]
399        for act in self.__initialActions:
400            self.addAction(act)
401
402    def showHistoryDialog(self):
403        """
404        Public slot to show the history dialog.
405        """
406        from .HistoryDialog import HistoryDialog
407        dlg = HistoryDialog(self.__mw)
408        dlg.openUrl.connect(self.openUrl)
409        dlg.newTab.connect(self.newTab)
410        dlg.newBackgroundTab.connect(self.newBackgroundTab)
411        dlg.newWindow.connect(self.newWindow)
412        dlg.newPrivateWindow.connect(self.newPrivateWindow)
413        dlg.show()
414
415    def __clearHistoryDialog(self):
416        """
417        Private slot to clear the history.
418        """
419        if self.__historyManager is not None and E5MessageBox.yesNo(
420                self,
421                self.tr("Clear History"),
422                self.tr("""Do you want to clear the history?""")):
423            self.__historyManager.clear()
424            self.__tabWidget.clearClosedTabsList()
425
426    def __aboutToShowClosedTabsMenu(self):
427        """
428        Private slot to populate the closed tabs menu.
429        """
430        fm = self.__closedTabsMenu.fontMetrics()
431        try:
432            maxWidth = fm.horizontalAdvance('m') * 40
433        except AttributeError:
434            maxWidth = fm.width('m') * 40
435
436        import WebBrowser.WebBrowserWindow
437        self.__closedTabsMenu.clear()
438        for index, tab in enumerate(
439            self.__tabWidget.closedTabsManager().allClosedTabs()
440        ):
441            title = fm.elidedText(tab.title, Qt.TextElideMode.ElideRight,
442                                  maxWidth)
443            act = self.__closedTabsMenu.addAction(
444                WebBrowser.WebBrowserWindow.WebBrowserWindow.icon(tab.url),
445                title)
446            act.setData(index)
447            act.triggered.connect(
448                functools.partial(self.__tabWidget.restoreClosedTab, act))
449        self.__closedTabsMenu.addSeparator()
450        self.__closedTabsMenu.addAction(
451            self.tr("Restore All Closed Tabs"),
452            self.__tabWidget.restoreAllClosedTabs)
453        self.__closedTabsMenu.addAction(
454            self.tr("Clear List"),
455            self.__tabWidget.clearClosedTabsList)
456
457    def __closedTabAvailable(self, avail):
458        """
459        Private slot to handle changes of the availability of closed tabs.
460
461        @param avail flag indicating the availability of closed tabs (boolean)
462        """
463        self.__closedTabsMenu.setEnabled(avail)
464
465
466class HistoryMostVisitedMenu(E5ModelMenu):
467    """
468    Class implementing the most visited history menu.
469
470    @signal openUrl(QUrl, str) emitted to open a URL in the current tab
471    @signal newTab(QUrl, str) emitted to open a URL in a new tab
472    @signal newBackgroundTab(QUrl, str) emitted to open a URL in a new
473        background tab
474    @signal newWindow(QUrl, str) emitted to open a URL in a new window
475    @signal newPrivateWindow(QUrl, str) emitted to open a URL in a new
476        private window
477    """
478    openUrl = pyqtSignal(QUrl, str)
479    newTab = pyqtSignal(QUrl, str)
480    newBackgroundTab = pyqtSignal(QUrl, str)
481    newWindow = pyqtSignal(QUrl, str)
482    newPrivateWindow = pyqtSignal(QUrl, str)
483
484    def __init__(self, count, parent=None):
485        """
486        Constructor
487
488        @param count maximum number of entries to be shown (integer)
489        @param parent reference to the parent widget (QWidget)
490        """
491        E5ModelMenu.__init__(self, parent)
492
493        self.__historyMenuModel = None
494
495        self.setMaxRows(count + 1)
496
497        self.setStatusBarTextRole(HistoryModel.UrlStringRole)
498
499    def __activated(self, idx):
500        """
501        Private slot handling the activated signal.
502
503        @param idx index of the activated item (QModelIndex)
504        """
505        if self._keyboardModifiers & Qt.KeyboardModifier.ControlModifier:
506            self.newTab.emit(
507                idx.data(HistoryModel.UrlRole),
508                idx.data(HistoryModel.TitleRole))
509        elif self._keyboardModifiers & Qt.KeyboardModifier.ShiftModifier:
510            self.newWindow.emit(
511                idx.data(HistoryModel.UrlRole),
512                idx.data(HistoryModel.TitleRole))
513        else:
514            self.openUrl.emit(
515                idx.data(HistoryModel.UrlRole),
516                idx.data(HistoryModel.TitleRole))
517
518    def prePopulated(self):
519        """
520        Public method to add any actions before the tree.
521
522        @return flag indicating if any actions were added (boolean)
523        """
524        if self.__historyMenuModel is None:
525            from WebBrowser.WebBrowserWindow import WebBrowserWindow
526            historyManager = WebBrowserWindow.historyManager()
527            self.__historyMenuModel = HistoryMostVisitedMenuModel(
528                historyManager.historyFilterModel(), self)
529            self.setModel(self.__historyMenuModel)
530        self.__historyMenuModel.sort(0)
531
532        return False
533