src/eric7/WebBrowser/TabManager/TabManagerWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9158
47c32c123843
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2016 - 2022 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 PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QTimer, QRect
19 from PyQt6.QtGui import QAction
20 from PyQt6.QtWidgets import (
21 QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QMenu, QStyle
22 )
23
24 from EricNetwork import EricTldExtractor, EricNetworkUtilities
25
26 from EricWidgets.EricApplication import ericApp
27 from EricWidgets.EricClickableLabel import EricClickableLabel
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 = EricTldExtractor.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 EricNetworkUtilities.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 addFlags = (
247 Qt.ItemFlag.ItemIsUserCheckable
248 if parent else
249 (Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsAutoTristate)
250 )
251 itm.setFlags(itm.flags() | addFlags)
252 itm.setCheckState(0, Qt.CheckState.Unchecked)
253
254 return itm
255
256 def __groupByDomainName(self, useHostName=False):
257 """
258 Private method to group the tree items by domain name.
259
260 @param useHostName flag indicating to use the host name
261 @type bool
262 """
263 windows = self.__mw.mainWindows()
264
265 tabsGroupedByDomain = {}
266
267 for mainWin in windows:
268 for browser in mainWin.tabWidget().browsers():
269 if self.__page == browser.page():
270 self.__page = None
271 continue
272 domain = self.domainFromUrl(browser.url(), useHostName)
273
274 if domain not in tabsGroupedByDomain:
275 groupItem = self.__createEmptyItem(None, False)
276 groupItem.setText(0, domain)
277 groupItem.setToolTip(0, domain)
278 font = groupItem.font(0)
279 font.setBold(True)
280 groupItem.setFont(0, font)
281 tabsGroupedByDomain[domain] = groupItem
282 groupItem = tabsGroupedByDomain[domain]
283
284 tabItem = self.__createEmptyItem(groupItem)
285 if browser == mainWin.tabWidget().currentBrowser():
286 font = tabItem.font(0)
287 font.setBold(True)
288 tabItem.setFont(0, font)
289 if not browser.isLoading():
290 tabItem.setIcon(0, browser.icon())
291 else:
292 tabItem.setIcon(0, UI.PixmapCache.getIcon("loading"))
293 tabItem.setText(0, browser.title())
294 tabItem.setToolTip(0, browser.title())
295
296 tabItem.setData(0, TabManagerWidget.WebBrowserRole, browser)
297 tabItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
298
299 self.__makeWebBrowserViewConnections(browser)
300
301 self.__tree.insertTopLevelItems(0, tabsGroupedByDomain.values())
302
303 def __groupByWindow(self):
304 """
305 Private method to group the tree items by window.
306 """
307 windows = self.__mw.mainWindows()
308
309 self.__isRefreshing = True
310
311 for winCount, mainWin in enumerate(windows, start=1):
312 winItem = self.__createEmptyItem()
313 winItem.setText(0, self.tr("Window {0}").format(winCount))
314 winItem.setToolTip(0, self.tr("Double click to switch"))
315 if mainWin == self.__mw:
316 font = winItem.font(0)
317 font.setBold(True)
318 winItem.setFont(0, font)
319 winItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
320
321 for browser in mainWin.tabWidget().browsers():
322 if self.__page == browser.page():
323 self.__page = None
324 continue
325
326 tabItem = self.__createEmptyItem(winItem)
327 if browser == mainWin.tabWidget().currentBrowser():
328 font = tabItem.font(0)
329 font.setBold(True)
330 tabItem.setFont(0, font)
331 if not browser.isLoading():
332 tabItem.setIcon(0, browser.icon())
333 else:
334 tabItem.setIcon(0, UI.PixmapCache.getIcon("loading"))
335 tabItem.setText(0, browser.title())
336 tabItem.setToolTip(0, browser.title())
337
338 tabItem.setData(0, TabManagerWidget.WebBrowserRole, browser)
339 tabItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
340
341 self.__makeWebBrowserViewConnections(browser)
342
343 def __makeWebBrowserViewConnections(self, view):
344 """
345 Private method to create the signal connections to the web view.
346
347 @param view reference to the web view
348 @type WebBrowserView
349 """
350 if view:
351 view.loadFinished.connect(self.delayedRefreshTree)
352 view.loadStarted.connect(self.delayedRefreshTree)
353 view.titleChanged.connect(self.delayedRefreshTree)
354 view.faviconChanged.connect(self.delayedRefreshTree)
355
356 @pyqtSlot()
357 def __refreshTree(self):
358 """
359 Private slot to referesh the tree.
360 """
361 if self.__refreshBlocked:
362 return
363
364 if self.__isRefreshing and not self.__page:
365 return
366
367 # store selected items
368 selectedBrowsers = []
369 for index in range(self.__tree.topLevelItemCount()):
370 winItem = self.__tree.topLevelItem(index)
371 if winItem.checkState(0) == Qt.CheckState.Unchecked:
372 continue
373
374 for row in range(winItem.childCount()):
375 tabItem = winItem.child(row)
376 if tabItem.checkState(0) == Qt.CheckState.Unchecked:
377 continue
378 selectedBrowsers.append(
379 tabItem.data(0, TabManagerWidget.WebBrowserRole))
380
381 self.__tree.clear()
382
383 if self.__groupType == TabManagerWidget.GroupByHost:
384 self.__groupByDomainName(True)
385 elif self.__groupType == TabManagerWidget.GroupByDomain:
386 self.__groupByDomainName()
387 else:
388 # default is group by window
389 self.__setGroupType(TabManagerWidget.GroupByWindow)
390 self.__groupByWindow()
391
392 # restore selected items
393 for index in range(self.__tree.topLevelItemCount()):
394 winItem = self.__tree.topLevelItem(index)
395
396 for row in range(winItem.childCount()):
397 tabItem = winItem.child(row)
398 if tabItem.data(0, TabManagerWidget.WebBrowserRole) in (
399 selectedBrowsers
400 ):
401 tabItem.setCheckState(0, Qt.CheckState.Checked)
402
403 self.__tree.expandAll()
404 self.__isRefreshing = False
405 self.__waitForRefresh = False
406
407 @pyqtSlot()
408 def __processActions(self, act):
409 """
410 Private slot to process the actions.
411
412 @param act reference to the action that triggered
413 @type QAction
414 """
415 self.__refreshBlocked = True
416
417 selectedBrowsers = collections.defaultdict(list)
418
419 command = act.objectName()
420
421 for index in range(self.__tree.topLevelItemCount()):
422 winItem = self.__tree.topLevelItem(index)
423 if winItem.checkState(0) == Qt.CheckState.Unchecked:
424 continue
425
426 for row in range(winItem.childCount()):
427 tabItem = winItem.child(row)
428 if tabItem.checkState(0) == Qt.CheckState.Unchecked:
429 continue
430
431 mainWin = tabItem.data(0, TabManagerWidget.WebWindowRole)
432 browser = tabItem.data(0, TabManagerWidget.WebBrowserRole)
433
434 selectedBrowsers[mainWin].append(browser)
435
436 winItem.setCheckState(0, Qt.CheckState.Unchecked)
437
438 if selectedBrowsers:
439 if command == "closeSelection":
440 self.closeSelectedBrowsers(selectedBrowsers)
441 elif command == "bookmarkSelection":
442 self.bookmarkSelectedBrowsers(selectedBrowsers)
443
444 self.__refreshBlocked = False
445 self.delayedRefreshTree()
446
447 @pyqtSlot(QTreeWidgetItem, int)
448 def __itemDoubleClicked(self, itm, column):
449 """
450 Private slot to handle double clicking a tree item.
451
452 @param itm reference to the item having been double clicked
453 @type QTreeWidgetItem
454 @param column column of the double click
455 @type int
456 """
457 if not itm:
458 return
459
460 mainWin = itm.data(0, TabManagerWidget.WebWindowRole)
461 browser = itm.data(0, TabManagerWidget.WebBrowserRole)
462
463 if not mainWin:
464 return
465
466 if mainWin.isMinimized():
467 mainWin.showNormal()
468 else:
469 mainWin.show()
470 mainWin.activateWindow()
471 mainWin.raise_()
472 mainWin.setFocus()
473
474 tabWidget = mainWin.tabWidget()
475 if browser and browser != tabWidget.currentWidget():
476 tabWidget.setCurrentWidget(browser)
477 browser.setFocus()
478
479 @pyqtSlot()
480 def __isBrowserSelected(self):
481 """
482 Private slot to check, if any browser entry is selected.
483
484 @return flag indicating the existence of a selected entry
485 @rtype bool
486 """
487 selected = False
488 for topRow in range(self.__tree.topLevelItemCount()):
489 topItm = self.__tree.topLevelItem(topRow)
490 if topItm.checkState(0) != Qt.CheckState.Unchecked:
491 selected = True
492 break
493
494 return selected
495
496 @pyqtSlot(QPoint)
497 def __customContextMenuRequested(self, pos):
498 """
499 Private slot to show the context menu.
500
501 @param pos position the menu should be shown at
502 @type QPoint
503 """
504 menu = QMenu()
505 groupTypeSubMenu = QMenu(self.tr("Group by"))
506 act = groupTypeSubMenu.addAction(self.tr("&Window"))
507 act.setData(TabManagerWidget.GroupByWindow)
508 act.setCheckable(True)
509 act.setChecked(self.__groupType == TabManagerWidget.GroupByWindow)
510
511 act = groupTypeSubMenu.addAction(self.tr("&Domain"))
512 act.setData(TabManagerWidget.GroupByDomain)
513 act.setCheckable(True)
514 act.setChecked(self.__groupType == TabManagerWidget.GroupByDomain)
515
516 act = groupTypeSubMenu.addAction(self.tr("&Host"))
517 act.setData(TabManagerWidget.GroupByHost)
518 act.setCheckable(True)
519 act.setChecked(self.__groupType == TabManagerWidget.GroupByHost)
520 groupTypeSubMenu.triggered.connect(self.changeGroupType)
521
522 menu.addMenu(groupTypeSubMenu)
523
524 menu.addSeparator()
525
526 if self.__isBrowserSelected():
527 act1 = menu.addAction(
528 UI.PixmapCache.getIcon("bookmark22"),
529 self.tr("&Bookmark checked tabs"))
530 act1.setObjectName("bookmarkSelection")
531 act1.triggered.connect(lambda: self.__processActions(act1))
532 act2 = menu.addAction(
533 UI.PixmapCache.getIcon("tabClose"),
534 self.tr("&Close checked tabs"))
535 act2.setObjectName("closeSelection")
536 act2.triggered.connect(lambda: self.__processActions(act2))
537
538 menu.exec(self.__tree.viewport().mapToGlobal(pos))
539
540 def mainWindowCreated(self, mainWin, refresh=True):
541 """
542 Public method to act on the creation of a new web browser window.
543
544 @param mainWin reference to the web browser window
545 @type WebBrowserWindow
546 @param refresh flag indicating to refresh the widget
547 @type bool
548 """
549 mainWin.webBrowserWindowClosed.connect(self.delayedRefreshTree)
550 mainWin.webBrowserWindowOpened.connect(self.mainWindowCreated)
551 mainWin.webBrowserOpened.connect(self.delayedRefreshTree)
552 mainWin.webBrowserClosed.connect(self.delayedRefreshTree)
553 mainWin.tabWidget().currentUrlChanged.connect(self.delayedRefreshTree)
554 mainWin.tabWidget().currentChanged.connect(self.delayedRefreshTree)
555
556 def createStatusBarIcon(self):
557 """
558 Public method to create a status bar icon.
559
560 @return generated icon
561 @rtype EricClickableLabel
562 """
563 icon = EricClickableLabel()
564 icon.setPixmap(
565 UI.PixmapCache.getPixmap("tabManager").scaled(16, 16))
566 icon.setToolTip(self.tr("Show Tab Manager"))
567 icon.clicked.connect(lambda: self.raiseTabManager(icon))
568
569 return icon
570
571 def raiseTabManager(self, icon):
572 """
573 Public slot to show the tab manager.
574
575 @param icon reference to the clicked icon
576 @type EricClickableLabel or QAction
577 """
578 window = None
579 if isinstance(icon, EricClickableLabel):
580 window = icon.window()
581 elif isinstance(icon, QAction):
582 window = icon.parent()
583
584 if window is not None:
585 titleBarHeight = self.style().pixelMetric(
586 QStyle.PixelMetric.PM_TitleBarHeight)
587
588 y = max(0, window.frameGeometry().top() + titleBarHeight + 1)
589
590 availableGeometry = ericApp().primaryScreen().availableGeometry()
591 windowFrameGeometry = window.frameGeometry()
592 if (availableGeometry.width() - windowFrameGeometry.right() - 1 >
593 self.frameGeometry().width()):
594 x = windowFrameGeometry.right() + 1
595 else:
596 x = windowFrameGeometry.x() - 1 - self.frameGeometry().width()
597
598 newGeo = QRect(x, y, self.width(), window.height())
599 self.setGeometry(newGeo)
600
601 self.activateWindow()
602 self.showNormal()
603 self.raise_()

eric ide

mercurial