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