|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the history menu. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import functools |
|
12 |
|
13 from PyQt5.QtCore import ( |
|
14 pyqtSignal, Qt, QMimeData, QUrl, QModelIndex, QSortFilterProxyModel, |
|
15 QAbstractProxyModel |
|
16 ) |
|
17 from PyQt5.QtWidgets import QMenu |
|
18 |
|
19 from E5Gui.E5ModelMenu import E5ModelMenu |
|
20 from E5Gui import E5MessageBox |
|
21 |
|
22 from .HistoryModel import HistoryModel |
|
23 |
|
24 import UI.PixmapCache |
|
25 |
|
26 |
|
27 class 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 |
|
231 class 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 |
|
273 class 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 |
|
466 class 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 |