eric7/WebBrowser/TabManager/TabManagerWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8218
7c09585bd960
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/WebBrowser/TabManager/TabManagerWidget.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,606 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a window for managing the web browser tabs.
+"""
+
+#
+# Modeled after the tab manager plug-in of Qupzilla
+# Copyright (C) 2013  S. Razi Alavizadeh <s.r.alavizadeh@gmail.com>
+#
+
+import os
+import collections
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QTimer, QRect
+from PyQt5.QtWidgets import (
+    QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QMenu, QStyle, QAction
+)
+
+import E5Network
+from E5Network import E5TldExtractor
+
+from E5Gui.E5Application import e5App
+from E5Gui.E5ClickableLabel import E5ClickableLabel
+
+import Utilities
+import UI.PixmapCache
+import Preferences
+
+
+class TabManagerWidget(QWidget):
+    """
+    Class implementing a window for managing the web browser tabs.
+    
+    @signal groupTypeChanged(int) emitted when the 'Group By' value was changed
+    """
+    GroupByWindow = 0
+    GroupByDomain = 1
+    GroupByHost = 2
+    
+    WebBrowserRole = Qt.ItemDataRole.UserRole + 1
+    WebWindowRole = Qt.ItemDataRole.UserRole + 2
+    
+    groupTypeChanged = pyqtSignal(int)
+    
+    _tldExtractor = None
+    
+    def __init__(self, mainWindow, parent=None, defaultWidget=False):
+        """
+        Constructor
+        
+        @param mainWindow reference to the main window
+        @type WebBrowserWindow
+        @param parent reference to the parent widget
+        @type QWidget
+        @param defaultWidget flag indicating the default widget
+        @type bool
+        """
+        super().__init__(parent)
+        self.setWindowFlags(Qt.WindowType.Window)
+        
+        self.__layout = QVBoxLayout(self)
+        self.__layout.setContentsMargins(0, 0, 0, 0)
+        self.__tree = QTreeWidget(self)
+        self.__tree.setHeaderHidden(True)
+        self.__tree.setExpandsOnDoubleClick(False)
+        self.__tree.setContextMenuPolicy(
+            Qt.ContextMenuPolicy.CustomContextMenu)
+        self.__layout.addWidget(self.__tree)
+        
+        self.setWindowTitle(self.tr("Tab Manager"))
+        
+        self.__mw = mainWindow
+        self.__page = None
+        
+        self.__isRefreshing = False
+        self.__refreshBlocked = False
+        self.__waitForRefresh = False
+        self.__isDefaultWidget = defaultWidget
+        self.__groupType = Preferences.getWebBrowser("TabManagerGroupByType")
+        
+        if TabManagerWidget._tldExtractor is None:
+            TabManagerWidget._tldExtractor = E5TldExtractor.instance()
+            TabManagerWidget._tldExtractor.setDataSearchPaths([
+                os.path.join(Utilities.getConfigDir(), "web_browser")])
+        
+        self.__tree.itemDoubleClicked.connect(self.__itemDoubleClicked)
+        self.__tree.customContextMenuRequested.connect(
+            self.__customContextMenuRequested)
+        
+        self.resize(400, 600)
+    
+    def closeSelectedBrowsers(self, browsersDict):
+        """
+        Public method to close the selected browsers.
+        
+        @param browsersDict dictionary containing the browsers per window
+        @type dict with WebBrowserWindow as key and list of WebBrowserView
+            as value
+        """
+        if not browsersDict:
+            return
+        
+        for mainWindow in browsersDict:
+            tabWidget = mainWindow.tabWidget()
+            for browser in browsersDict[mainWindow]:
+                tabWidget.closeBrowserAt(tabWidget.indexOf(browser))
+    
+    def bookmarkSelectedBrowsers(self, browsersDict):
+        """
+        Public method to bookmark the selected browsers.
+        
+        @param browsersDict dictionary containing the browsers per window
+        @type dict with WebBrowserWindow as key and list of WebBrowserView
+            as value
+        """
+        if not browsersDict:
+            return
+        
+        from ..Bookmarks.BookmarkNode import BookmarkNode
+        from ..Bookmarks.AddBookmarkDialog import AddBookmarkDialog
+        
+        dlg = AddBookmarkDialog()
+        dlg.setFolder(True)
+        dlg.setTitle(self.tr("Saved Tabs"))
+        dlg.exec()
+        
+        folder = dlg.addedNode()
+        if folder is None:
+            return
+        
+        for mainWin in browsersDict:
+            for browser in browsersDict[mainWin]:
+                if (
+                    not browser.url().isEmpty() and
+                    browser.url().scheme() != "eric"
+                ):
+                    bookmark = BookmarkNode(BookmarkNode.Bookmark)
+                    bookmark.url = bytes(browser.url().toEncoded()).decode()
+                    bookmark.title = browser.title()
+                    
+                    self.__mw.bookmarksManager().addBookmark(folder, bookmark)
+    
+    def __setGroupType(self, groupType):
+        """
+        Private method to set the 'Group By' type.
+        
+        @param groupType 'Group By' type to be set
+        @type int (0 - 2)
+        """
+        self.__groupType = groupType
+        Preferences.setWebBrowser("TabManagerGroupByType", groupType)
+    
+    def domainFromUrl(self, url, useHostName=False):
+        """
+        Public method to extract the domain from an URL.
+        
+        @param url URL to extract the domain from
+        @type QUrl
+        @param useHostName flag indicating to use the host name
+        @type bool
+        @return domain name
+        @rtype str
+        """
+        appendStr = ":"
+        urlString = url.toString()
+        
+        if url.scheme() == "file":
+            return self.tr("Local File System:")
+        elif url.scheme() == "eric" or not urlString:
+            return self.tr("eric Web Browser:")
+        elif url.scheme() == "ftp":
+            appendStr = self.tr(" [FTP]:")
+        
+        host = url.host()
+        if not host:
+            return urlString + appendStr
+        
+        if useHostName or E5Network.isValidAddress(host):
+            if host.lower().startswith("www."):
+                host = host[4:]
+        else:
+            registeredDomain = (
+                TabManagerWidget._tldExtractor.registrableDomain(host)
+            )
+            if registeredDomain:
+                host = registeredDomain
+        
+        return host + appendStr
+    
+    def delayedRefreshTree(self, page=None):
+        """
+        Public slot to do a delyed refresh of the tree.
+        
+        @param page reference to the web page
+        @type WebBrowserPage
+        """
+        if self.__refreshBlocked or self.__waitForRefresh:
+            return
+        
+        if self.__isRefreshing and not page:
+            return
+        
+        self.__page = page
+        self.__waitForRefresh = True
+        QTimer.singleShot(50, self.__refreshTree)
+    
+    def changeGroupType(self, act):
+        """
+        Public slot to change the 'Group By' type.
+        
+        @param act reference to the action that was triggered
+        @type QAction
+        """
+        if act is None:
+            return
+        
+        groupType = act.data()
+        if self.__groupType != groupType:
+            self.__setGroupType(groupType)
+            self.delayedRefreshTree()
+            self.groupTypeChanged.emit(self.__groupType)
+    
+    def __createEmptyItem(self, parent=None, addToTree=True):
+        """
+        Private method to create an empty tree item.
+        
+        @param parent reference to the parent item
+        @type QTreeWidgetItem or QTreeWidget
+        @param addToTree flag indicating to add the item to the tree
+        @type bool
+        @return created item
+        @rtype QTreeWidgetItem
+        """
+        if addToTree:
+            if parent:
+                parentItem = parent
+            else:
+                parentItem = self.__tree.invisibleRootItem()
+        else:
+            parentItem = None
+        itm = QTreeWidgetItem(parentItem)
+        flags = itm.flags()
+        if parent:
+            flags |= Qt.ItemFlag.ItemIsUserCheckable
+        else:
+            flags |= (
+                Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsTristate
+            )
+        itm.setFlags(itm.flags() | flags)
+        itm.setCheckState(0, Qt.CheckState.Unchecked)
+        
+        return itm
+    
+    def __groupByDomainName(self, useHostName=False):
+        """
+        Private method to group the tree items by domain name.
+        
+        @param useHostName flag indicating to use the host name
+        @type bool
+        """
+        windows = self.__mw.mainWindows()
+        
+        tabsGroupedByDomain = {}
+        
+        for mainWin in windows:
+            for browser in mainWin.tabWidget().browsers():
+                if self.__page == browser.page():
+                    self.__page = None
+                    continue
+                domain = self.domainFromUrl(browser.url(), useHostName)
+                
+                if domain not in tabsGroupedByDomain:
+                    groupItem = self.__createEmptyItem(None, False)
+                    groupItem.setText(0, domain)
+                    groupItem.setToolTip(0, domain)
+                    font = groupItem.font(0)
+                    font.setBold(True)
+                    groupItem.setFont(0, font)
+                    tabsGroupedByDomain[domain] = groupItem
+                groupItem = tabsGroupedByDomain[domain]
+                
+                tabItem = self.__createEmptyItem(groupItem)
+                if browser == mainWin.tabWidget().currentBrowser():
+                    font = tabItem.font(0)
+                    font.setBold(True)
+                    tabItem.setFont(0, font)
+                if not browser.isLoading():
+                    tabItem.setIcon(0, browser.icon())
+                else:
+                    tabItem.setIcon(0, UI.PixmapCache.getIcon("loading"))
+                tabItem.setText(0, browser.title())
+                tabItem.setToolTip(0, browser.title())
+                
+                tabItem.setData(0, TabManagerWidget.WebBrowserRole, browser)
+                tabItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
+                
+                self.__makeWebBrowserViewConnections(browser)
+        
+        self.__tree.insertTopLevelItems(0, tabsGroupedByDomain.values())
+    
+    def __groupByWindow(self):
+        """
+        Private method to group the tree items by window.
+        """
+        windows = self.__mw.mainWindows()
+        
+        self.__isRefreshing = True
+        
+        for winCount, mainWin in enumerate(windows, start=1):
+            winItem = self.__createEmptyItem()
+            winItem.setText(0, self.tr("Window {0}").format(winCount))
+            winItem.setToolTip(0, self.tr("Double click to switch"))
+            if mainWin == self.__mw:
+                font = winItem.font(0)
+                font.setBold(True)
+                winItem.setFont(0, font)
+            winItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
+            
+            for browser in mainWin.tabWidget().browsers():
+                if self.__page == browser.page():
+                    self.__page = None
+                    continue
+                
+                tabItem = self.__createEmptyItem(winItem)
+                if browser == mainWin.tabWidget().currentBrowser():
+                    font = tabItem.font(0)
+                    font.setBold(True)
+                    tabItem.setFont(0, font)
+                if not browser.isLoading():
+                    tabItem.setIcon(0, browser.icon())
+                else:
+                    tabItem.setIcon(0, UI.PixmapCache.getIcon("loading"))
+                tabItem.setText(0, browser.title())
+                tabItem.setToolTip(0, browser.title())
+                
+                tabItem.setData(0, TabManagerWidget.WebBrowserRole, browser)
+                tabItem.setData(0, TabManagerWidget.WebWindowRole, mainWin)
+                
+                self.__makeWebBrowserViewConnections(browser)
+    
+    def __makeWebBrowserViewConnections(self, view):
+        """
+        Private method to create the signal connections to the web view.
+        
+        @param view reference to the web view
+        @type WebBrowserView
+        """
+        if view:
+            view.loadFinished.connect(self.delayedRefreshTree)
+            view.loadStarted.connect(self.delayedRefreshTree)
+            view.titleChanged.connect(self.delayedRefreshTree)
+            view.faviconChanged.connect(self.delayedRefreshTree)
+    
+    @pyqtSlot()
+    def __refreshTree(self):
+        """
+        Private slot to referesh the tree.
+        """
+        if self.__refreshBlocked:
+            return
+        
+        if self.__isRefreshing and not self.__page:
+            return
+        
+        # store selected items
+        selectedBrowsers = []
+        for index in range(self.__tree.topLevelItemCount()):
+            winItem = self.__tree.topLevelItem(index)
+            if winItem.checkState(0) == Qt.CheckState.Unchecked:
+                continue
+            
+            for row in range(winItem.childCount()):
+                tabItem = winItem.child(row)
+                if tabItem.checkState(0) == Qt.CheckState.Unchecked:
+                    continue
+                selectedBrowsers.append(
+                    tabItem.data(0, TabManagerWidget.WebBrowserRole))
+        
+        self.__tree.clear()
+        
+        if self.__groupType == TabManagerWidget.GroupByHost:
+            self.__groupByDomainName(True)
+        elif self.__groupType == TabManagerWidget.GroupByDomain:
+            self.__groupByDomainName()
+        else:
+            # default is group by window
+            self.__setGroupType(TabManagerWidget.GroupByWindow)
+            self.__groupByWindow()
+        
+        # restore selected items
+        for index in range(self.__tree.topLevelItemCount()):
+            winItem = self.__tree.topLevelItem(index)
+            
+            for row in range(winItem.childCount()):
+                tabItem = winItem.child(row)
+                if tabItem.data(0, TabManagerWidget.WebBrowserRole) in (
+                    selectedBrowsers
+                ):
+                    tabItem.setCheckState(0, Qt.CheckState.Checked)
+        
+        self.__tree.expandAll()
+        self.__isRefreshing = False
+        self.__waitForRefresh = False
+    
+    @pyqtSlot()
+    def __processActions(self, act):
+        """
+        Private slot to process the actions.
+        
+        @param act reference to the action that triggered
+        @type QAction
+        """
+        self.__refreshBlocked = True
+        
+        selectedBrowsers = collections.defaultdict(list)
+        
+        command = act.objectName()
+        
+        for index in range(self.__tree.topLevelItemCount()):
+            winItem = self.__tree.topLevelItem(index)
+            if winItem.checkState(0) == Qt.CheckState.Unchecked:
+                continue
+            
+            for row in range(winItem.childCount()):
+                tabItem = winItem.child(row)
+                if tabItem.checkState(0) == Qt.CheckState.Unchecked:
+                    continue
+                
+                mainWin = tabItem.data(0, TabManagerWidget.WebWindowRole)
+                browser = tabItem.data(0, TabManagerWidget.WebBrowserRole)
+                
+                selectedBrowsers[mainWin].append(browser)
+            
+            winItem.setCheckState(0, Qt.CheckState.Unchecked)
+        
+        if selectedBrowsers:
+            if command == "closeSelection":
+                self.closeSelectedBrowsers(selectedBrowsers)
+            elif command == "bookmarkSelection":
+                self.bookmarkSelectedBrowsers(selectedBrowsers)
+        
+        self.__refreshBlocked = False
+        self.delayedRefreshTree()
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def __itemDoubleClicked(self, itm, column):
+        """
+        Private slot to handle double clicking a tree item.
+        
+        @param itm reference to the item having been double clicked
+        @type QTreeWidgetItem
+        @param column column of the double click
+        @type int
+        """
+        if not itm:
+            return
+        
+        mainWin = itm.data(0, TabManagerWidget.WebWindowRole)
+        browser = itm.data(0, TabManagerWidget.WebBrowserRole)
+        
+        if not mainWin:
+            return
+        
+        if mainWin.isMinimized():
+            mainWin.showNormal()
+        else:
+            mainWin.show()
+        mainWin.activateWindow()
+        mainWin.raise_()
+        mainWin.setFocus()
+        
+        tabWidget = mainWin.tabWidget()
+        if browser and browser != tabWidget.currentWidget():
+            tabWidget.setCurrentWidget(browser)
+            browser.setFocus()
+    
+    @pyqtSlot()
+    def __isBrowserSelected(self):
+        """
+        Private slot to check, if any browser entry is selected.
+        
+        @return flag indicating the existence of a selected entry
+        @rtype bool
+        """
+        selected = False
+        for topRow in range(self.__tree.topLevelItemCount()):
+            topItm = self.__tree.topLevelItem(topRow)
+            if topItm.checkState(0) != Qt.CheckState.Unchecked:
+                selected = True
+                break
+        
+        return selected
+    
+    @pyqtSlot(QPoint)
+    def __customContextMenuRequested(self, pos):
+        """
+        Private slot to show the context menu.
+        
+        @param pos position the menu should be shown at
+        @type QPoint
+        """
+        menu = QMenu()
+        groupTypeSubMenu = QMenu(self.tr("Group by"))
+        act = groupTypeSubMenu.addAction(self.tr("&Window"))
+        act.setData(TabManagerWidget.GroupByWindow)
+        act.setCheckable(True)
+        act.setChecked(self.__groupType == TabManagerWidget.GroupByWindow)
+        
+        act = groupTypeSubMenu.addAction(self.tr("&Domain"))
+        act.setData(TabManagerWidget.GroupByDomain)
+        act.setCheckable(True)
+        act.setChecked(self.__groupType == TabManagerWidget.GroupByDomain)
+        
+        act = groupTypeSubMenu.addAction(self.tr("&Host"))
+        act.setData(TabManagerWidget.GroupByHost)
+        act.setCheckable(True)
+        act.setChecked(self.__groupType == TabManagerWidget.GroupByHost)
+        groupTypeSubMenu.triggered.connect(self.changeGroupType)
+        
+        menu.addMenu(groupTypeSubMenu)
+        
+        menu.addSeparator()
+        
+        if self.__isBrowserSelected():
+            act1 = menu.addAction(
+                UI.PixmapCache.getIcon("bookmark22"),
+                self.tr("&Bookmark checked tabs"))
+            act1.setObjectName("bookmarkSelection")
+            act1.triggered.connect(lambda: self.__processActions(act1))
+            act2 = menu.addAction(
+                UI.PixmapCache.getIcon("tabClose"),
+                self.tr("&Close checked tabs"))
+            act2.setObjectName("closeSelection")
+            act2.triggered.connect(lambda: self.__processActions(act2))
+        
+        menu.exec(self.__tree.viewport().mapToGlobal(pos))
+    
+    def mainWindowCreated(self, mainWin, refresh=True):
+        """
+        Public method to act on the creation of a new web browser window.
+        
+        @param mainWin reference to the web browser window
+        @type WebBrowserWindow
+        @param refresh flag indicating to refresh the widget
+        @type bool
+        """
+        mainWin.webBrowserWindowClosed.connect(self.delayedRefreshTree)
+        mainWin.webBrowserWindowOpened.connect(self.mainWindowCreated)
+        mainWin.webBrowserOpened.connect(self.delayedRefreshTree)
+        mainWin.webBrowserClosed.connect(self.delayedRefreshTree)
+        mainWin.tabWidget().currentUrlChanged.connect(self.delayedRefreshTree)
+        mainWin.tabWidget().currentChanged.connect(self.delayedRefreshTree)
+    
+    def createStatusBarIcon(self):
+        """
+        Public method to create a status bar icon.
+        
+        @return generated icon
+        @rtype E5ClickableLabel
+        """
+        icon = E5ClickableLabel()
+        icon.setPixmap(
+            UI.PixmapCache.getPixmap("tabManager").scaled(16, 16))
+        icon.setToolTip(self.tr("Show Tab Manager"))
+        icon.clicked.connect(lambda: self.raiseTabManager(icon))
+        
+        return icon
+    
+    def raiseTabManager(self, icon):
+        """
+        Public slot to show the tab manager.
+        
+        @param icon reference to the clicked icon
+        @type E5ClickableLabel or QAction
+        """
+        window = None
+        if isinstance(icon, E5ClickableLabel):
+            window = icon.window()
+        elif isinstance(icon, QAction):
+            window = icon.parentWidget()
+        
+        if window is not None:
+            titleBarHeight = self.style().pixelMetric(
+                QStyle.PixelMetric.PM_TitleBarHeight)
+            
+            y = max(0, window.frameGeometry().top() + titleBarHeight + 1)
+            
+            desktop = e5App().desktop()
+            desktopGeometry = desktop.availableGeometry(self)
+            windowFrameGeometry = window.frameGeometry()
+            if (desktopGeometry.width() - windowFrameGeometry.right() - 1 >
+                    self.frameGeometry().width()):
+                x = windowFrameGeometry.right() + 1
+            else:
+                x = windowFrameGeometry.x() - 1 - self.frameGeometry().width()
+            
+            newGeo = QRect(x, y, self.width(), window.height())
+            self.setGeometry(newGeo)
+        
+        self.activateWindow()
+        self.showNormal()
+        self.raise_()

eric ide

mercurial