eric7/WebBrowser/TabManager/TabManagerWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8218
7c09585bd960
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a window for managing the web browser tabs.
8 """
9
10 #
11 # Modeled after the tab manager plug-in of Qupzilla
12 # Copyright (C) 2013 S. Razi Alavizadeh <s.r.alavizadeh@gmail.com>
13 #
14
15 import os
16 import collections
17
18 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QTimer, QRect
19 from PyQt5.QtWidgets import (
20 QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QMenu, QStyle, QAction
21 )
22
23 import E5Network
24 from E5Network import E5TldExtractor
25
26 from E5Gui.E5Application import e5App
27 from E5Gui.E5ClickableLabel import E5ClickableLabel
28
29 import Utilities
30 import UI.PixmapCache
31 import Preferences
32
33
34 class TabManagerWidget(QWidget):
35 """
36 Class implementing a window for managing the web browser tabs.
37
38 @signal groupTypeChanged(int) emitted when the 'Group By' value was changed
39 """
40 GroupByWindow = 0
41 GroupByDomain = 1
42 GroupByHost = 2
43
44 WebBrowserRole = Qt.ItemDataRole.UserRole + 1
45 WebWindowRole = Qt.ItemDataRole.UserRole + 2
46
47 groupTypeChanged = pyqtSignal(int)
48
49 _tldExtractor = None
50
51 def __init__(self, mainWindow, parent=None, defaultWidget=False):
52 """
53 Constructor
54
55 @param mainWindow reference to the main window
56 @type WebBrowserWindow
57 @param parent reference to the parent widget
58 @type QWidget
59 @param defaultWidget flag indicating the default widget
60 @type bool
61 """
62 super().__init__(parent)
63 self.setWindowFlags(Qt.WindowType.Window)
64
65 self.__layout = QVBoxLayout(self)
66 self.__layout.setContentsMargins(0, 0, 0, 0)
67 self.__tree = QTreeWidget(self)
68 self.__tree.setHeaderHidden(True)
69 self.__tree.setExpandsOnDoubleClick(False)
70 self.__tree.setContextMenuPolicy(
71 Qt.ContextMenuPolicy.CustomContextMenu)
72 self.__layout.addWidget(self.__tree)
73
74 self.setWindowTitle(self.tr("Tab Manager"))
75
76 self.__mw = mainWindow
77 self.__page = None
78
79 self.__isRefreshing = False
80 self.__refreshBlocked = False
81 self.__waitForRefresh = False
82 self.__isDefaultWidget = defaultWidget
83 self.__groupType = Preferences.getWebBrowser("TabManagerGroupByType")
84
85 if TabManagerWidget._tldExtractor is None:
86 TabManagerWidget._tldExtractor = E5TldExtractor.instance()
87 TabManagerWidget._tldExtractor.setDataSearchPaths([
88 os.path.join(Utilities.getConfigDir(), "web_browser")])
89
90 self.__tree.itemDoubleClicked.connect(self.__itemDoubleClicked)
91 self.__tree.customContextMenuRequested.connect(
92 self.__customContextMenuRequested)
93
94 self.resize(400, 600)
95
96 def closeSelectedBrowsers(self, browsersDict):
97 """
98 Public method to close the selected browsers.
99
100 @param browsersDict dictionary containing the browsers per window
101 @type dict with WebBrowserWindow as key and list of WebBrowserView
102 as value
103 """
104 if not browsersDict:
105 return
106
107 for mainWindow in browsersDict:
108 tabWidget = mainWindow.tabWidget()
109 for browser in browsersDict[mainWindow]:
110 tabWidget.closeBrowserAt(tabWidget.indexOf(browser))
111
112 def bookmarkSelectedBrowsers(self, browsersDict):
113 """
114 Public method to bookmark the selected browsers.
115
116 @param browsersDict dictionary containing the browsers per window
117 @type dict with WebBrowserWindow as key and list of WebBrowserView
118 as value
119 """
120 if not browsersDict:
121 return
122
123 from ..Bookmarks.BookmarkNode import BookmarkNode
124 from ..Bookmarks.AddBookmarkDialog import AddBookmarkDialog
125
126 dlg = AddBookmarkDialog()
127 dlg.setFolder(True)
128 dlg.setTitle(self.tr("Saved Tabs"))
129 dlg.exec()
130
131 folder = dlg.addedNode()
132 if folder is None:
133 return
134
135 for mainWin in browsersDict:
136 for browser in browsersDict[mainWin]:
137 if (
138 not browser.url().isEmpty() and
139 browser.url().scheme() != "eric"
140 ):
141 bookmark = BookmarkNode(BookmarkNode.Bookmark)
142 bookmark.url = bytes(browser.url().toEncoded()).decode()
143 bookmark.title = browser.title()
144
145 self.__mw.bookmarksManager().addBookmark(folder, bookmark)
146
147 def __setGroupType(self, groupType):
148 """
149 Private method to set the 'Group By' type.
150
151 @param groupType 'Group By' type to be set
152 @type int (0 - 2)
153 """
154 self.__groupType = groupType
155 Preferences.setWebBrowser("TabManagerGroupByType", groupType)
156
157 def domainFromUrl(self, url, useHostName=False):
158 """
159 Public method to extract the domain from an URL.
160
161 @param url URL to extract the domain from
162 @type QUrl
163 @param useHostName flag indicating to use the host name
164 @type bool
165 @return domain name
166 @rtype str
167 """
168 appendStr = ":"
169 urlString = url.toString()
170
171 if url.scheme() == "file":
172 return self.tr("Local File System:")
173 elif url.scheme() == "eric" or not urlString:
174 return self.tr("eric Web Browser:")
175 elif url.scheme() == "ftp":
176 appendStr = self.tr(" [FTP]:")
177
178 host = url.host()
179 if not host:
180 return urlString + appendStr
181
182 if useHostName or E5Network.isValidAddress(host):
183 if host.lower().startswith("www."):
184 host = host[4:]
185 else:
186 registeredDomain = (
187 TabManagerWidget._tldExtractor.registrableDomain(host)
188 )
189 if registeredDomain:
190 host = registeredDomain
191
192 return host + appendStr
193
194 def delayedRefreshTree(self, page=None):
195 """
196 Public slot to do a delyed refresh of the tree.
197
198 @param page reference to the web page
199 @type WebBrowserPage
200 """
201 if self.__refreshBlocked or self.__waitForRefresh:
202 return
203
204 if self.__isRefreshing and not page:
205 return
206
207 self.__page = page
208 self.__waitForRefresh = True
209 QTimer.singleShot(50, self.__refreshTree)
210
211 def changeGroupType(self, act):
212 """
213 Public slot to change the 'Group By' type.
214
215 @param act reference to the action that was triggered
216 @type QAction
217 """
218 if act is None:
219 return
220
221 groupType = act.data()
222 if self.__groupType != groupType:
223 self.__setGroupType(groupType)
224 self.delayedRefreshTree()
225 self.groupTypeChanged.emit(self.__groupType)
226
227 def __createEmptyItem(self, parent=None, addToTree=True):
228 """
229 Private method to create an empty tree item.
230
231 @param parent reference to the parent item
232 @type QTreeWidgetItem or QTreeWidget
233 @param addToTree flag indicating to add the item to the tree
234 @type bool
235 @return created item
236 @rtype QTreeWidgetItem
237 """
238 if addToTree:
239 if parent:
240 parentItem = parent
241 else:
242 parentItem = self.__tree.invisibleRootItem()
243 else:
244 parentItem = None
245 itm = QTreeWidgetItem(parentItem)
246 flags = itm.flags()
247 if parent:
248 flags |= Qt.ItemFlag.ItemIsUserCheckable
249 else:
250 flags |= (
251 Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsTristate
252 )
253 itm.setFlags(itm.flags() | flags)
254 itm.setCheckState(0, Qt.CheckState.Unchecked)
255
256 return itm
257
258 def __groupByDomainName(self, useHostName=False):
259 """
260 Private method to group the tree items by domain name.
261
262 @param useHostName flag indicating to use the host name
263 @type bool
264 """
265 windows = self.__mw.mainWindows()
266
267 tabsGroupedByDomain = {}
268
269 for mainWin in windows:
270 for browser in mainWin.tabWidget().browsers():
271 if self.__page == browser.page():
272 self.__page = None
273 continue
274 domain = self.domainFromUrl(browser.url(), useHostName)
275
276 if domain not in tabsGroupedByDomain:
277 groupItem = self.__createEmptyItem(None, False)
278 groupItem.setText(0, domain)
279 groupItem.setToolTip(0, domain)
280 font = groupItem.font(0)
281 font.setBold(True)
282 groupItem.setFont(0, font)
283 tabsGroupedByDomain[domain] = groupItem
284 groupItem = tabsGroupedByDomain[domain]
285
286 tabItem = self.__createEmptyItem(groupItem)
287 if browser == mainWin.tabWidget().currentBrowser():
288 font = tabItem.font(0)
289 font.setBold(True)
290 tabItem.setFont(0, font)
291 if not browser.isLoading():
292 tabItem.setIcon(0, browser.icon())
293 else:
294 tabItem.setIcon(0, UI.PixmapCache.getIcon("loading"))
295 tabItem.setText(0, browser.title())
296 tabItem.setToolTip(0, browser.title())
297
298 tabItem.setData(0, TabManagerWidget.WebBrowserRole, browser)
299 tabItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
300
301 self.__makeWebBrowserViewConnections(browser)
302
303 self.__tree.insertTopLevelItems(0, tabsGroupedByDomain.values())
304
305 def __groupByWindow(self):
306 """
307 Private method to group the tree items by window.
308 """
309 windows = self.__mw.mainWindows()
310
311 self.__isRefreshing = True
312
313 for winCount, mainWin in enumerate(windows, start=1):
314 winItem = self.__createEmptyItem()
315 winItem.setText(0, self.tr("Window {0}").format(winCount))
316 winItem.setToolTip(0, self.tr("Double click to switch"))
317 if mainWin == self.__mw:
318 font = winItem.font(0)
319 font.setBold(True)
320 winItem.setFont(0, font)
321 winItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
322
323 for browser in mainWin.tabWidget().browsers():
324 if self.__page == browser.page():
325 self.__page = None
326 continue
327
328 tabItem = self.__createEmptyItem(winItem)
329 if browser == mainWin.tabWidget().currentBrowser():
330 font = tabItem.font(0)
331 font.setBold(True)
332 tabItem.setFont(0, font)
333 if not browser.isLoading():
334 tabItem.setIcon(0, browser.icon())
335 else:
336 tabItem.setIcon(0, UI.PixmapCache.getIcon("loading"))
337 tabItem.setText(0, browser.title())
338 tabItem.setToolTip(0, browser.title())
339
340 tabItem.setData(0, TabManagerWidget.WebBrowserRole, browser)
341 tabItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
342
343 self.__makeWebBrowserViewConnections(browser)
344
345 def __makeWebBrowserViewConnections(self, view):
346 """
347 Private method to create the signal connections to the web view.
348
349 @param view reference to the web view
350 @type WebBrowserView
351 """
352 if view:
353 view.loadFinished.connect(self.delayedRefreshTree)
354 view.loadStarted.connect(self.delayedRefreshTree)
355 view.titleChanged.connect(self.delayedRefreshTree)
356 view.faviconChanged.connect(self.delayedRefreshTree)
357
358 @pyqtSlot()
359 def __refreshTree(self):
360 """
361 Private slot to referesh the tree.
362 """
363 if self.__refreshBlocked:
364 return
365
366 if self.__isRefreshing and not self.__page:
367 return
368
369 # store selected items
370 selectedBrowsers = []
371 for index in range(self.__tree.topLevelItemCount()):
372 winItem = self.__tree.topLevelItem(index)
373 if winItem.checkState(0) == Qt.CheckState.Unchecked:
374 continue
375
376 for row in range(winItem.childCount()):
377 tabItem = winItem.child(row)
378 if tabItem.checkState(0) == Qt.CheckState.Unchecked:
379 continue
380 selectedBrowsers.append(
381 tabItem.data(0, TabManagerWidget.WebBrowserRole))
382
383 self.__tree.clear()
384
385 if self.__groupType == TabManagerWidget.GroupByHost:
386 self.__groupByDomainName(True)
387 elif self.__groupType == TabManagerWidget.GroupByDomain:
388 self.__groupByDomainName()
389 else:
390 # default is group by window
391 self.__setGroupType(TabManagerWidget.GroupByWindow)
392 self.__groupByWindow()
393
394 # restore selected items
395 for index in range(self.__tree.topLevelItemCount()):
396 winItem = self.__tree.topLevelItem(index)
397
398 for row in range(winItem.childCount()):
399 tabItem = winItem.child(row)
400 if tabItem.data(0, TabManagerWidget.WebBrowserRole) in (
401 selectedBrowsers
402 ):
403 tabItem.setCheckState(0, Qt.CheckState.Checked)
404
405 self.__tree.expandAll()
406 self.__isRefreshing = False
407 self.__waitForRefresh = False
408
409 @pyqtSlot()
410 def __processActions(self, act):
411 """
412 Private slot to process the actions.
413
414 @param act reference to the action that triggered
415 @type QAction
416 """
417 self.__refreshBlocked = True
418
419 selectedBrowsers = collections.defaultdict(list)
420
421 command = act.objectName()
422
423 for index in range(self.__tree.topLevelItemCount()):
424 winItem = self.__tree.topLevelItem(index)
425 if winItem.checkState(0) == Qt.CheckState.Unchecked:
426 continue
427
428 for row in range(winItem.childCount()):
429 tabItem = winItem.child(row)
430 if tabItem.checkState(0) == Qt.CheckState.Unchecked:
431 continue
432
433 mainWin = tabItem.data(0, TabManagerWidget.WebWindowRole)
434 browser = tabItem.data(0, TabManagerWidget.WebBrowserRole)
435
436 selectedBrowsers[mainWin].append(browser)
437
438 winItem.setCheckState(0, Qt.CheckState.Unchecked)
439
440 if selectedBrowsers:
441 if command == "closeSelection":
442 self.closeSelectedBrowsers(selectedBrowsers)
443 elif command == "bookmarkSelection":
444 self.bookmarkSelectedBrowsers(selectedBrowsers)
445
446 self.__refreshBlocked = False
447 self.delayedRefreshTree()
448
449 @pyqtSlot(QTreeWidgetItem, int)
450 def __itemDoubleClicked(self, itm, column):
451 """
452 Private slot to handle double clicking a tree item.
453
454 @param itm reference to the item having been double clicked
455 @type QTreeWidgetItem
456 @param column column of the double click
457 @type int
458 """
459 if not itm:
460 return
461
462 mainWin = itm.data(0, TabManagerWidget.WebWindowRole)
463 browser = itm.data(0, TabManagerWidget.WebBrowserRole)
464
465 if not mainWin:
466 return
467
468 if mainWin.isMinimized():
469 mainWin.showNormal()
470 else:
471 mainWin.show()
472 mainWin.activateWindow()
473 mainWin.raise_()
474 mainWin.setFocus()
475
476 tabWidget = mainWin.tabWidget()
477 if browser and browser != tabWidget.currentWidget():
478 tabWidget.setCurrentWidget(browser)
479 browser.setFocus()
480
481 @pyqtSlot()
482 def __isBrowserSelected(self):
483 """
484 Private slot to check, if any browser entry is selected.
485
486 @return flag indicating the existence of a selected entry
487 @rtype bool
488 """
489 selected = False
490 for topRow in range(self.__tree.topLevelItemCount()):
491 topItm = self.__tree.topLevelItem(topRow)
492 if topItm.checkState(0) != Qt.CheckState.Unchecked:
493 selected = True
494 break
495
496 return selected
497
498 @pyqtSlot(QPoint)
499 def __customContextMenuRequested(self, pos):
500 """
501 Private slot to show the context menu.
502
503 @param pos position the menu should be shown at
504 @type QPoint
505 """
506 menu = QMenu()
507 groupTypeSubMenu = QMenu(self.tr("Group by"))
508 act = groupTypeSubMenu.addAction(self.tr("&Window"))
509 act.setData(TabManagerWidget.GroupByWindow)
510 act.setCheckable(True)
511 act.setChecked(self.__groupType == TabManagerWidget.GroupByWindow)
512
513 act = groupTypeSubMenu.addAction(self.tr("&Domain"))
514 act.setData(TabManagerWidget.GroupByDomain)
515 act.setCheckable(True)
516 act.setChecked(self.__groupType == TabManagerWidget.GroupByDomain)
517
518 act = groupTypeSubMenu.addAction(self.tr("&Host"))
519 act.setData(TabManagerWidget.GroupByHost)
520 act.setCheckable(True)
521 act.setChecked(self.__groupType == TabManagerWidget.GroupByHost)
522 groupTypeSubMenu.triggered.connect(self.changeGroupType)
523
524 menu.addMenu(groupTypeSubMenu)
525
526 menu.addSeparator()
527
528 if self.__isBrowserSelected():
529 act1 = menu.addAction(
530 UI.PixmapCache.getIcon("bookmark22"),
531 self.tr("&Bookmark checked tabs"))
532 act1.setObjectName("bookmarkSelection")
533 act1.triggered.connect(lambda: self.__processActions(act1))
534 act2 = menu.addAction(
535 UI.PixmapCache.getIcon("tabClose"),
536 self.tr("&Close checked tabs"))
537 act2.setObjectName("closeSelection")
538 act2.triggered.connect(lambda: self.__processActions(act2))
539
540 menu.exec(self.__tree.viewport().mapToGlobal(pos))
541
542 def mainWindowCreated(self, mainWin, refresh=True):
543 """
544 Public method to act on the creation of a new web browser window.
545
546 @param mainWin reference to the web browser window
547 @type WebBrowserWindow
548 @param refresh flag indicating to refresh the widget
549 @type bool
550 """
551 mainWin.webBrowserWindowClosed.connect(self.delayedRefreshTree)
552 mainWin.webBrowserWindowOpened.connect(self.mainWindowCreated)
553 mainWin.webBrowserOpened.connect(self.delayedRefreshTree)
554 mainWin.webBrowserClosed.connect(self.delayedRefreshTree)
555 mainWin.tabWidget().currentUrlChanged.connect(self.delayedRefreshTree)
556 mainWin.tabWidget().currentChanged.connect(self.delayedRefreshTree)
557
558 def createStatusBarIcon(self):
559 """
560 Public method to create a status bar icon.
561
562 @return generated icon
563 @rtype E5ClickableLabel
564 """
565 icon = E5ClickableLabel()
566 icon.setPixmap(
567 UI.PixmapCache.getPixmap("tabManager").scaled(16, 16))
568 icon.setToolTip(self.tr("Show Tab Manager"))
569 icon.clicked.connect(lambda: self.raiseTabManager(icon))
570
571 return icon
572
573 def raiseTabManager(self, icon):
574 """
575 Public slot to show the tab manager.
576
577 @param icon reference to the clicked icon
578 @type E5ClickableLabel or QAction
579 """
580 window = None
581 if isinstance(icon, E5ClickableLabel):
582 window = icon.window()
583 elif isinstance(icon, QAction):
584 window = icon.parentWidget()
585
586 if window is not None:
587 titleBarHeight = self.style().pixelMetric(
588 QStyle.PixelMetric.PM_TitleBarHeight)
589
590 y = max(0, window.frameGeometry().top() + titleBarHeight + 1)
591
592 desktop = e5App().desktop()
593 desktopGeometry = desktop.availableGeometry(self)
594 windowFrameGeometry = window.frameGeometry()
595 if (desktopGeometry.width() - windowFrameGeometry.right() - 1 >
596 self.frameGeometry().width()):
597 x = windowFrameGeometry.right() + 1
598 else:
599 x = windowFrameGeometry.x() - 1 - self.frameGeometry().width()
600
601 newGeo = QRect(x, y, self.width(), window.height())
602 self.setGeometry(newGeo)
603
604 self.activateWindow()
605 self.showNormal()
606 self.raise_()

eric ide

mercurial