eric6/WebBrowser/TabManager/TabManagerWidget.py

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

eric ide

mercurial