eric7/HelpViewer/HelpViewerWidget.py

Thu, 30 Dec 2021 11:17:58 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 30 Dec 2021 11:17:58 +0100
branch
eric7
changeset 8881
54e42bc2437a
parent 8874
ccd99849803e
child 8895
d66181594630
permissions
-rw-r--r--

Updated copyright for 2022.

# -*- coding: utf-8 -*-

# Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing an embedded viewer for QtHelp and local HTML files.
"""

import os

from PyQt6.QtCore import pyqtSlot, Qt, QUrl, QTimer, QByteArray
from PyQt6.QtGui import QAction, QFont, QFontMetrics
from PyQt6.QtHelp import QHelpEngine
from PyQt6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout, QComboBox, QSizePolicy, QStackedWidget,
    QToolButton, QButtonGroup, QAbstractButton, QMenu, QFrame, QLabel,
    QProgressBar, QSplitter
)
try:
    from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings
    WEBENGINE_AVAILABLE = True
except ImportError:
    WEBENGINE_AVAILABLE = False

from EricWidgets import EricFileDialog, EricMessageBox
from EricWidgets.EricTextEditSearchWidget import (
    EricTextEditSearchWidget, EricTextEditType
)

import UI.PixmapCache
import Utilities
import Preferences

from .OpenPagesWidget import OpenPagesWidget

from WebBrowser.QtHelp.HelpTocWidget import HelpTocWidget
from WebBrowser.QtHelp.HelpIndexWidget import HelpIndexWidget
from WebBrowser.QtHelp.HelpSearchWidget import HelpSearchWidget


# TODO: add support for bookmarks
class HelpViewerWidget(QWidget):
    """
    Class implementing an embedded viewer for QtHelp and local HTML files.
    """
    MaxHistoryItems = 20    # max. number of history items to be shown
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget (defaults to None)
        @type QWidget (optional)
        """
        super().__init__(parent)
        self.setObjectName("HelpViewerWidget")
        
        self.__ui = parent
        
        self.__initHelpEngine()
        
        self.__layout = QVBoxLayout()
        self.__layout.setObjectName("MainLayout")
        self.__layout.setContentsMargins(0, 3, 0, 0)
        
        ###################################################################
        ## Help Topic Selector
        ###################################################################
        
        self.__selectorLayout = QHBoxLayout()
        
        self.__helpSelector = QComboBox(self)
        self.__helpSelector.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.__selectorLayout.addWidget(self.__helpSelector)
        self.__populateHelpSelector()
        self.__helpSelector.activated.connect(self.__helpTopicSelected)
        
        self.__openButton = QToolButton(self)
        self.__openButton.setIcon(UI.PixmapCache.getIcon("open"))
        self.__openButton.setToolTip(self.tr("Open a local file"))
        self.__openButton.clicked.connect(self.__openFile)
        self.__selectorLayout.addWidget(self.__openButton)
        
        self.__actionsButton = QToolButton(self)
        self.__actionsButton.setIcon(
            UI.PixmapCache.getIcon("actionsToolButton"))
        self.__actionsButton.setToolTip(
            self.tr("Select action from menu"))
        self.__actionsButton.setPopupMode(
            QToolButton.ToolButtonPopupMode.InstantPopup)
        self.__selectorLayout.addWidget(self.__actionsButton)
        
        self.__layout.addLayout(self.__selectorLayout)
        
        ###################################################################
        ## Navigation Buttons
        ###################################################################
        
        self.__navButtonsLayout = QHBoxLayout()
        
        self.__navButtonsLayout.addStretch()
        
        self.__backwardButton = QToolButton(self)
        self.__backwardButton.setIcon(UI.PixmapCache.getIcon("back"))
        self.__backwardButton.setToolTip(self.tr("Move one page backward"))
        self.__backwardButton.clicked.connect(self.__backward)
        
        self.__forwardButton = QToolButton(self)
        self.__forwardButton.setIcon(UI.PixmapCache.getIcon("forward"))
        self.__forwardButton.setToolTip(self.tr("Move one page forward"))
        self.__forwardButton.clicked.connect(self.__forward)
        
        self.__backForButtonLayout = QHBoxLayout()
        self.__backForButtonLayout.setContentsMargins(0, 0, 0, 0)
        self.__backForButtonLayout.setSpacing(0)
        self.__backForButtonLayout.addWidget(self.__backwardButton)
        self.__backForButtonLayout.addWidget(self.__forwardButton)
        self.__navButtonsLayout.addLayout(self.__backForButtonLayout)
        
        self.__reloadButton = QToolButton(self)
        self.__reloadButton.setIcon(UI.PixmapCache.getIcon("reload"))
        self.__reloadButton.setToolTip(self.tr("Reload the current page"))
        self.__reloadButton.clicked.connect(self.__reload)
        self.__navButtonsLayout.addWidget(self.__reloadButton)
        
        self.__buttonLine1 = QFrame(self)
        self.__buttonLine1.setFrameShape(QFrame.Shape.VLine)
        self.__buttonLine1.setFrameShadow(QFrame.Shadow.Sunken)
        self.__navButtonsLayout.addWidget(self.__buttonLine1)
        
        self.__zoomInButton = QToolButton(self)
        self.__zoomInButton.setIcon(UI.PixmapCache.getIcon("zoomIn"))
        self.__zoomInButton.setToolTip(
            self.tr("Zoom in on the current page"))
        self.__zoomInButton.clicked.connect(self.__zoomIn)
        self.__navButtonsLayout.addWidget(self.__zoomInButton)
        
        self.__zoomOutButton = QToolButton(self)
        self.__zoomOutButton.setIcon(UI.PixmapCache.getIcon("zoomOut"))
        self.__zoomOutButton.setToolTip(
            self.tr("Zoom out on the current page"))
        self.__zoomOutButton.clicked.connect(self.__zoomOut)
        self.__navButtonsLayout.addWidget(self.__zoomOutButton)
        
        self.__zoomResetButton = QToolButton(self)
        self.__zoomResetButton.setIcon(UI.PixmapCache.getIcon("zoomReset"))
        self.__zoomResetButton.setToolTip(
            self.tr("Reset the zoom level of the current page"))
        self.__zoomResetButton.clicked.connect(self.__zoomReset)
        self.__navButtonsLayout.addWidget(self.__zoomResetButton)
        
        self.__buttonLine2 = QFrame(self)
        self.__buttonLine2.setFrameShape(QFrame.Shape.VLine)
        self.__buttonLine2.setFrameShadow(QFrame.Shadow.Sunken)
        self.__navButtonsLayout.addWidget(self.__buttonLine2)
        
        self.__addPageButton = QToolButton(self)
        self.__addPageButton.setIcon(UI.PixmapCache.getIcon("plus"))
        self.__addPageButton.setToolTip(
            self.tr("Add a new empty page"))
        self.__addPageButton.clicked.connect(self.__addNewPage)
        self.__navButtonsLayout.addWidget(self.__addPageButton)
        
        self.__closePageButton = QToolButton(self)
        self.__closePageButton.setIcon(UI.PixmapCache.getIcon("minus"))
        self.__closePageButton.setToolTip(
            self.tr("Close the current page"))
        self.__closePageButton.clicked.connect(self.closeCurrentPage)
        self.__navButtonsLayout.addWidget(self.__closePageButton)
        
        self.__buttonLine3 = QFrame(self)
        self.__buttonLine3.setFrameShape(QFrame.Shape.VLine)
        self.__buttonLine3.setFrameShadow(QFrame.Shadow.Sunken)
        self.__navButtonsLayout.addWidget(self.__buttonLine3)
        
        self.__searchButton = QToolButton(self)
        self.__searchButton.setIcon(UI.PixmapCache.getIcon("find"))
        self.__searchButton.setToolTip(
            self.tr("Show or hide the search pane"))
        self.__searchButton.setCheckable(True)
        self.__searchButton.setChecked(False)
        self.__searchButton.clicked.connect(self.showHideSearch)
        self.__navButtonsLayout.addWidget(self.__searchButton)
        
        self.__navButtonsLayout.addStretch()
        
        self.__layout.addLayout(self.__navButtonsLayout)
                
        self.__backMenu = QMenu(self)
        self.__backMenu.triggered.connect(self.__navigationMenuActionTriggered)
        self.__backwardButton.setMenu(self.__backMenu)
        self.__backMenu.aboutToShow.connect(self.__showBackMenu)
        
        self.__forwardMenu = QMenu(self)
        self.__forwardMenu.triggered.connect(
            self.__navigationMenuActionTriggered)
        self.__forwardButton.setMenu(self.__forwardMenu)
        self.__forwardMenu.aboutToShow.connect(self.__showForwardMenu)

        ###################################################################
        ## Center widget with help pages, search widget and navigation
        ## widgets
        ###################################################################
        
        self.__centerSplitter = QSplitter(Qt.Orientation.Vertical, self)
        self.__centerSplitter.setChildrenCollapsible(False)
        self.__layout.addWidget(self.__centerSplitter)
        
        self.__helpCenterWidget = QWidget(self)
        self.__helpCenterLayout = QVBoxLayout()
        self.__helpCenterLayout.setContentsMargins(0, 0, 0, 0)
        self.__helpCenterWidget.setLayout(self.__helpCenterLayout)
        
        ###################################################################
        
        self.__helpStack = QStackedWidget(self)
        self.__helpStack.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.__helpCenterLayout.addWidget(self.__helpStack)
        
        ###################################################################
        
        self.__searchWidget = EricTextEditSearchWidget(
            self, widthForHeight=False, enableClose=True)
        self.__helpCenterLayout.addWidget(self.__searchWidget)
        self.__searchWidget.closePressed.connect(self.__searchWidgetClosed)
        self.__searchWidget.hide()
        
        self.__centerSplitter.addWidget(self.__helpCenterWidget)
        
        ###################################################################
        
        self.__helpNavigationStack = QStackedWidget(self)
        self.__helpNavigationStack.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.__helpNavigationStack.setMinimumHeight(100)
        self.__centerSplitter.addWidget(self.__helpNavigationStack)
        self.__populateNavigationStack()
        
        ###################################################################
        ## Bottom buttons
        ###################################################################
        
        self.__buttonLayout = QHBoxLayout()
        
        self.__buttonGroup = QButtonGroup(self)
        self.__buttonGroup.setExclusive(True)
        self.__buttonGroup.buttonClicked.connect(
            self.__selectNavigationWidget)
        
        self.__buttonLayout.addStretch()
        
        self.__openPagesButton = self.__addNavigationButton(
            "fileMisc", self.tr("Show list of open pages"))
        self.__helpTocButton = self.__addNavigationButton(
            "tableOfContents", self.tr("Show the table of contents"))
        self.__helpIndexButton = self.__addNavigationButton(
            "helpIndex", self.tr("Show the help document index"))
        self.__helpSearchButton = self.__addNavigationButton(
            "documentFind", self.tr("Show the help search window"))
        self.__openPagesButton.setChecked(True)
        
        self.__buttonLayout.addStretch()
        
        self.__helpFilterWidget = self.__initFilterWidget()
        self.__buttonLayout.addWidget(self.__helpFilterWidget)
        
        self.__layout.addLayout(self.__buttonLayout)
        
        self.__indexingProgressWidget = self.__initIndexingProgress()
        self.__layout.addWidget(self.__indexingProgressWidget)
        self.__indexingProgressWidget.hide()
        
        ###################################################################
        
        self.setLayout(self.__layout)
        
        self.__openPagesButton.setChecked(True)
        
        self.__ui.preferencesChanged.connect(self.__populateHelpSelector)
        
        self.__initActionsMenu()
        
        if WEBENGINE_AVAILABLE:
            self.__initQWebEngine()
            self.__ui.preferencesChanged.connect(self.__initQWebEngineSettings)
        
        self.addPage()
        self.__checkActionButtons()
        
        self.__centerSplitter.setSizes([900, 150])
        
        QTimer.singleShot(50, self.__lookForNewDocumentation)
    
    def __addNavigationButton(self, iconName, toolTip):
        """
        Private method to create and add a navigation button.
        
        @param iconName name of the icon
        @type str
        @param toolTip tooltip to be shown
        @type str
        @return reference to the created button
        @rtype QToolButton
        """
        button = QToolButton(self)
        button.setIcon(UI.PixmapCache.getIcon(iconName))
        button.setToolTip(toolTip)
        button.setCheckable(True)
        self.__buttonGroup.addButton(button)
        self.__buttonLayout.addWidget(button)
        
        return button
    
    def __populateNavigationStack(self):
        """
        Private method to populate the stack of navigation widgets.
        """
        # Open Pages
        self.__openPagesList = OpenPagesWidget(self.__helpStack, self)
        self.__openPagesList.currentChanged.connect(self.__currentPageChanged)
        self.__helpNavigationStack.addWidget(self.__openPagesList)
        
        # QtHelp TOC widget
        self.__helpTocWidget = HelpTocWidget(
            self.__helpEngine, internal=True)
        self.__helpTocWidget.escapePressed.connect(self.__activateCurrentPage)
        self.__helpTocWidget.openUrl.connect(self.openUrl)
        self.__helpTocWidget.newTab.connect(self.openUrlNewPage)
        self.__helpTocWidget.newBackgroundTab.connect(
            self.openUrlNewBackgroundPage)
        self.__helpNavigationStack.addWidget(self.__helpTocWidget)
        
        # QtHelp Index widget
        self.__helpIndexWidget = HelpIndexWidget(
            self.__helpEngine, internal=True)
        self.__helpIndexWidget.escapePressed.connect(
            self.__activateCurrentPage)
        self.__helpIndexWidget.openUrl.connect(self.openUrl)
        self.__helpIndexWidget.newTab.connect(self.openUrlNewPage)
        self.__helpIndexWidget.newBackgroundTab.connect(
            self.openUrlNewBackgroundPage)
        self.__helpNavigationStack.addWidget(self.__helpIndexWidget)
        
        # QtHelp Search widget
        self.__indexing = False
        self.__indexingProgress = None
        self.__helpSearchEngine = self.__helpEngine.searchEngine()
        self.__helpSearchEngine.indexingStarted.connect(
            self.__indexingStarted)
        self.__helpSearchEngine.indexingFinished.connect(
            self.__indexingFinished)
        
        self.__helpSearchWidget = HelpSearchWidget(
            self.__helpSearchEngine, internal=True)
        self.__helpSearchWidget.escapePressed.connect(
            self.__activateCurrentPage)
        self.__helpSearchWidget.openUrl.connect(self.openUrl)
        self.__helpSearchWidget.newTab.connect(self.openUrlNewPage)
        self.__helpSearchWidget.newBackgroundTab.connect(
            self.openUrlNewBackgroundPage)
        self.__helpNavigationStack.addWidget(self.__helpSearchWidget)
    
    @pyqtSlot(QAbstractButton)
    def __selectNavigationWidget(self, button):
        """
        Private slot to select the navigation widget.
        
        @param button reference to the clicked button
        @type QAbstractButton
        """
        if button == self.__openPagesButton:
            self.__helpNavigationStack.setCurrentWidget(
                self.__openPagesList)
        elif button == self.__helpTocButton:
            self.__helpNavigationStack.setCurrentWidget(
                self.__helpTocWidget)
        elif button == self.__helpIndexButton:
            self.__helpNavigationStack.setCurrentWidget(
                self.__helpIndexWidget)
        elif button == self.__helpSearchButton:
            self.__helpNavigationStack.setCurrentWidget(
                self.__helpSearchWidget)
    
    def __populateHelpSelector(self):
        """
        Private method to populate the help selection combo box.
        """
        self.__helpSelector.clear()
        
        self.__helpSelector.addItem("", "")
        
        for key, topic in [
            ("EricDocDir", self.tr("eric API Documentation")),
            ("PythonDocDir", self.tr("Python 3 Documentation")),
            ("Qt5DocDir", self.tr("Qt5 Documentation")),
            ("Qt6DocDir", self.tr("Qt6 Documentation")),
            ("PyQt5DocDir", self.tr("PyQt5 Documentation")),
            ("PyQt6DocDir", self.tr("PyQt6 Documentation")),
            ("PySide2DocDir", self.tr("PySide2 Documentation")),
            ("PySide6DocDir", self.tr("PySide6 Documentation")),
        ]:
            urlStr = Preferences.getHelp(key)
            if urlStr:
                self.__helpSelector.addItem(topic, urlStr)
    
    @pyqtSlot()
    def __helpTopicSelected(self):
        """
        Private slot handling the selection of a new help topic.
        """
        urlStr = self.__helpSelector.currentData()
        if urlStr:
            url = QUrl(urlStr)
            self.openUrl(url)
    
    def activate(self, searchWord=None, url=None):
        """
        Public method to activate the widget and search for a given word.
        
        @param searchWord word to search for (defaults to None)
        @type str (optional)
        @param url URL to show in a new page
        @type QUrl
        """
        if url is not None:
            cv = self.currentViewer()
            if cv and cv.isEmptyPage():
                self.openUrl(url)
            else:
                self.openUrlNewPage(url)
        else:
            cv = self.currentViewer()
            if cv:
                cv.setFocus(Qt.FocusReason.OtherFocusReason)
            
            if searchWord:
                self.searchQtHelp(searchWord)
    
    def shutdown(self):
        """
        Public method to perform shut down actions.
        """
        self.__helpSearchEngine.cancelIndexing()
        self.__helpSearchEngine.cancelSearching()
        
        self.__helpInstaller.stop()
    
    @pyqtSlot()
    def __openFile(self):
        """
        Private slot to open a local help file (*.html).
        """
        htmlFile = EricFileDialog.getOpenFileName(
            self,
            self.tr("Open HTML File"),
            "",
            self.tr("HTML Files (*.htm *.html);;All Files (*)")
        )
        if htmlFile:
            self.currentViewer().setLink(QUrl.fromLocalFile(htmlFile))
    
    @pyqtSlot()
    def __addNewPage(self):
        """
        Private slot to add a new empty page.
        """
        self.addPage()
    
    def addPage(self, url=None, background=False):
        """
        Public method to add a new help page with the given URL.
        
        @param url requested URL (defaults to QUrl("about:blank"))
        @type QUrl (optional)
        @param background flag indicating to open the page in the background
            (defaults to False)
        @type bool (optional)
        @return reference to the created page
        @rtype HelpViewerImpl
        """
        if url is None:
            url = QUrl("about:blank")
        
        viewer, viewerType = self.__newViewer()
        viewer.setLink(url)
        
        cv = self.currentViewer()
        if background and bool(cv):
            index = self.__helpStack.indexOf(cv) + 1
            self.__helpStack.insertWidget(index, viewer)
            self.__openPagesList.insertPage(
                index, viewer, background=background)
            cv.setFocus(Qt.FocusReason.OtherFocusReason)
        else:
            self.__helpStack.addWidget(viewer)
            self.__openPagesList.addPage(viewer, background=background)
            viewer.setFocus(Qt.FocusReason.OtherFocusReason)
            self.__searchWidget.attachTextEdit(viewer, editType=viewerType)
        
        return viewer
    
    @pyqtSlot(QUrl)
    def openUrl(self, url):
        """
        Public slot to load a URL in the current page.
        
        @param url URL to be opened
        @type QUrl
        """
        cv = self.currentViewer()
        if cv:
            cv.setLink(url)
            cv.setFocus(Qt.FocusReason.OtherFocusReason)
    
    @pyqtSlot(QUrl)
    def openUrlNewPage(self, url):
        """
        Public slot to load a URL in a new page.
        
        @param url URL to be opened
        @type QUrl
        """
        self.addPage(url=url)
    
    @pyqtSlot(QUrl)
    def openUrlNewBackgroundPage(self, url):
        """
        Public slot to load a URL in a new background page.
        
        @param url URL to be opened
        @type QUrl
        """
        self.addPage(url=url, background=True)
    
    @pyqtSlot()
    def closeCurrentPage(self):
        """
        Public slot to close the current page.
        """
        self.__openPagesList.closeCurrentPage()
    
    @pyqtSlot()
    def closeOtherPages(self):
        """
        Public slot to close all other pages.
        """
        self.__openPagesList.closeOtherPages()
    
    @pyqtSlot()
    def closeAllPages(self):
        """
        Public slot to close all pages.
        """
        self.__openPagesList.closeAllPages()
    
    @pyqtSlot()
    def __activateCurrentPage(self):
        """
        Private slot to activate the current page.
        """
        cv = self.currentViewer()
        if cv:
            cv.setFocus()
    
    def __newViewer(self):
        """
        Private method to create a new help viewer.
        
        @return tuple containing the reference to the created help viewer
            object and its type
        @rtype tuple of (HelpViewerImpl, EricTextEditType)
        """
        if WEBENGINE_AVAILABLE:
            from .HelpViewerImplQWE import HelpViewerImplQWE
            viewer = HelpViewerImplQWE(self.__helpEngine, self)
            viewerType = EricTextEditType.QWEBENGINEVIEW
        else:
            from .HelpViewerImplQTB import HelpViewerImplQTB
            viewer = HelpViewerImplQTB(self.__helpEngine, self)
            viewerType = EricTextEditType.QTEXTBROWSER
        
        viewer.zoomChanged.connect(self.__checkActionButtons)
        
        return viewer, viewerType
    
    def currentViewer(self):
        """
        Public method to get the active viewer.
        
        @return reference to the active help viewer
        @rtype HelpViewerImpl
        """
        return self.__helpStack.currentWidget()
    
    #######################################################################
    ## QtHelp related code below
    #######################################################################
    
    def __initHelpEngine(self):
        """
        Private method to initialize the QtHelp related stuff.
        """
        self.__helpEngine = QHelpEngine(
            self.__getQtHelpCollectionFileName(),
            self)
        self.__helpEngine.setReadOnly(False)
        self.__helpEngine.setUsesFilterEngine(True)
        
        self.__helpEngine.warning.connect(self.__warning)
        
        self.__helpEngine.setupData()
        self.__removeOldDocumentation()
    
    def __getQtHelpCollectionFileName(self):
        """
        Private method to determine the name of the QtHelp collection file.
        
        @return path of the QtHelp collection file
        @rtype str
        """
        qthelpDir = os.path.join(Utilities.getConfigDir(), "qthelp")
        if not os.path.exists(qthelpDir):
            os.makedirs(qthelpDir)
        return os.path.join(qthelpDir, "eric7help.qhc")
    
    @pyqtSlot(str)
    def __warning(self, msg):
        """
        Private slot handling warnings of the help engine.
        
        @param msg message sent by the help  engine
        @type str
        """
        EricMessageBox.warning(
            self,
            self.tr("Help Engine"), msg)
    
    @pyqtSlot()
    def __removeOldDocumentation(self):
        """
        Private slot to remove non-existing documentation from the help engine.
        """
        for namespace in self.__helpEngine.registeredDocumentations():
            docFile = self.__helpEngine.documentationFileName(namespace)
            if not os.path.exists(docFile):
                self.__helpEngine.unregisterDocumentation(namespace)
    
    @pyqtSlot()
    def __lookForNewDocumentation(self):
        """
        Private slot to look for new documentation to be loaded into the
        help database.
        """
        from WebBrowser.QtHelp.HelpDocsInstaller import HelpDocsInstaller
        self.__helpInstaller = HelpDocsInstaller(
            self.__helpEngine.collectionFile())
        self.__helpInstaller.errorMessage.connect(
            self.__showInstallationError)
        self.__helpInstaller.docsInstalled.connect(self.__docsInstalled)
        
        self.__ui.statusBar().showMessage(
            self.tr("Looking for Documentation..."))
        self.__helpInstaller.installDocs()
    
    @pyqtSlot(str)
    def __showInstallationError(self, message):
        """
        Private slot to show installation errors.
        
        @param message message to be shown
        @type str
        """
        EricMessageBox.warning(
            self,
            self.tr("eric Help Viewer"),
            message)
    
    @pyqtSlot(bool)
    def __docsInstalled(self, installed):
        """
        Private slot handling the end of documentation installation.
        
        @param installed flag indicating that documents were installed
        @type bool
        """
        self.__ui.statusBar().clearMessage()
        self.__helpEngine.setupData()
    
    #######################################################################
    ## Actions Menu related methods
    #######################################################################
    
    def __initActionsMenu(self):
        """
        Private method to initialize the actions menu.
        """
        self.__actionsMenu = QMenu()
        self.__actionsMenu.setToolTipsVisible(True)
        
        self.__actionsMenu.addAction(
            self.tr("Manage QtHelp Documents"),
            self.__manageQtHelpDocuments)
        self.__actionsMenu.addAction(
            self.tr("Reindex Documentation"),
            self.__helpSearchEngine.reindexDocumentation)
        self.__actionsMenu.addSeparator()
        self.__actionsMenu.addAction(
            self.tr("Configure Help Documentation"),
            self.__configureHelpDocumentation)
        
        self.__actionsButton.setMenu(self.__actionsMenu)
    
    @pyqtSlot()
    def __manageQtHelpDocuments(self):
        """
        Private slot to manage the QtHelp documentation database.
        """
        from WebBrowser.QtHelp.QtHelpDocumentationConfigurationDialog import (
            QtHelpDocumentationConfigurationDialog
        )
        dlg = QtHelpDocumentationConfigurationDialog(
            self.__helpEngine, self)
        dlg.exec()
    
    @pyqtSlot()
    def __configureHelpDocumentation(self):
        """
        Private slot to open the Help Documentation configuration page.
        """
        self.__ui.showPreferences("helpDocumentationPage")
    
    #######################################################################
    ## Navigation related methods below
    #######################################################################
    
    @pyqtSlot()
    def __backward(self):
        """
        Private slot to move one page backward.
        """
        cv = self.currentViewer()
        if cv:
            cv.backward()
    
    @pyqtSlot()
    def __forward(self):
        """
        Private slot to move one page foreward.
        """
        cv = self.currentViewer()
        if cv:
            cv.forward()
    
    @pyqtSlot()
    def __reload(self):
        """
        Private slot to reload the current page.
        """
        cv = self.currentViewer()
        if cv:
            cv.reload()
    
    def __showBackMenu(self):
        """
        Private slot showing the backward navigation menu.
        """
        cv = self.currentViewer()
        if cv:
            self.__backMenu.clear()
            backwardHistoryCount = min(cv.backwardHistoryCount(),
                                       HelpViewerWidget.MaxHistoryItems)
            
            for index in range(1, backwardHistoryCount + 1):
                act = QAction(self)
                act.setData(-index)
                act.setText(cv.historyTitle(-index))
                self.__backMenu.addAction(act)
            
            self.__backMenu.addSeparator()
            self.__backMenu.addAction(self.tr("Clear History"),
                                      self.__clearHistory)
    
    def __showForwardMenu(self):
        """
        Private slot showing the forward navigation menu.
        """
        cv = self.currentViewer()
        if cv:
            self.__forwardMenu.clear()
            forwardHistoryCount = min(cv.forwardHistoryCount(),
                                      HelpViewerWidget.MaxHistoryItems)
        
            for index in range(1, forwardHistoryCount + 1):
                act = QAction(self)
                act.setData(index)
                act.setText(cv.historyTitle(index))
                self.__forwardMenu.addAction(act)
            
            self.__forwardMenu.addSeparator()
            self.__forwardMenu.addAction(self.tr("Clear History"),
                                         self.__clearHistory)
    
    def __navigationMenuActionTriggered(self, act):
        """
        Private slot to go to the selected page.
        
        @param act reference to the action selected in the navigation menu
        @type QAction
        """
        cv = self.currentViewer()
        if cv:
            index = act.data()
            if index is not None:
                cv.gotoHistory(index)
    
    def __clearHistory(self):
        """
        Private slot to clear the history of the current viewer.
        """
        cv = self.currentViewer()
        if cv:
            cv.clearHistory()
            self.__checkActionButtons()
    
    #######################################################################
    ## Page navigation related methods below
    #######################################################################
    
    @pyqtSlot()
    def __checkActionButtons(self):
        """
        Private slot to set the enabled state of the action buttons.
        """
        cv = self.currentViewer()
        if cv:
            self.__backwardButton.setEnabled(cv.isBackwardAvailable())
            self.__forwardButton.setEnabled(cv.isForwardAvailable())
            self.__zoomInButton.setEnabled(cv.isScaleUpAvailable())
            self.__zoomOutButton.setEnabled(cv.isScaleDownAvailable())
        else:
            self.__backwardButton.setEnabled(False)
            self.__forwardButton.setEnabled(False)
            self.__zoomInButton.setEnabled(False)
            self.__zoomOutButton.setEnabled(False)
    
    @pyqtSlot()
    def __currentPageChanged(self):
        """
        Private slot handling the selection of another page.
        """
        self.__checkActionButtons()
        cv = self.currentViewer()
        if cv:
            self.__searchWidget.attachTextEdit(
                cv,
                editType=(
                    EricTextEditType.QWEBENGINEVIEW
                    if WEBENGINE_AVAILABLE else
                    EricTextEditType.QTEXTBROWSER
                )
            )
            cv.setFocus(Qt.FocusReason.OtherFocusReason)
    
    #######################################################################
    ## Zoom related methods below
    #######################################################################
    
    @pyqtSlot()
    def __zoomIn(self):
        """
        Private slot to zoom in.
        """
        cv = self.currentViewer()
        if cv:
            cv.scaleUp()
    
    @pyqtSlot()
    def __zoomOut(self):
        """
        Private slot to zoom out.
        """
        cv = self.currentViewer()
        if cv:
            cv.scaleDown()
    
    @pyqtSlot()
    def __zoomReset(self):
        """
        Private slot to reset the zoom level.
        """
        cv = self.currentViewer()
        if cv:
            cv.resetScale()
    
    #######################################################################
    ## QtHelp Search related methods below
    #######################################################################
    
    def __initIndexingProgress(self):
        """
        Private method to initialize the help documents indexing progress
        widget.
        
        @return reference to the generated widget
        @rtype QWidget
        """
        progressWidget = QWidget(self)
        layout = QHBoxLayout(progressWidget)
        layout.setContentsMargins(0, 0, 0, 0)
        
        label = QLabel(self.tr("Updating search index"))
        layout.addWidget(label)
        
        progressBar = QProgressBar()
        progressBar.setRange(0, 0)
        progressBar.setTextVisible(False)
        progressBar.setFixedHeight(16)
        layout.addWidget(progressBar)
        
        return progressWidget
    
    @pyqtSlot()
    def __indexingStarted(self):
        """
        Private slot handling the start of the indexing process.
        """
        self.__indexing = True
        self.__indexingProgressWidget.show()
    
    @pyqtSlot()
    def __indexingFinished(self):
        """
        Private slot handling the end of the indexing process.
        """
        self.__indexingProgressWidget.hide()
        self.__indexing = False
    
    @pyqtSlot(str)
    def searchQtHelp(self, searchExpression):
        """
        Public slot to search for a given search expression.
        
        @param searchExpression expression to search for
        @type str
        """
        if searchExpression:
            if self.__indexing:
                # Try again a second later
                QTimer.singleShot(
                    1000,
                    lambda: self.searchQtHelp(searchExpression)
                )
            else:
                self.__helpSearchButton.setChecked(True)
                self.__helpSearchEngine.search(searchExpression)
    
    #######################################################################
    ## QtHelp filter related methods below
    #######################################################################
    
    def __initFilterWidget(self):
        """
        Private method to initialize the filter selection widget.
        
        @return reference to the generated widget
        @rtype QWidget
        """
        filterWidget = QWidget()
        layout = QHBoxLayout(filterWidget)
        layout.setContentsMargins(0, 0, 0, 0)
        
        label = QLabel(self.tr("Filtered by: "))
        layout.addWidget(label)
        
        self.__helpFilterCombo = QComboBox()
        comboWidth = QFontMetrics(QFont()).horizontalAdvance(
            "ComboBoxWithEnoughWidth")
        self.__helpFilterCombo.setMinimumWidth(comboWidth)
        layout.addWidget(self.__helpFilterCombo)
        
        self.__helpEngine.setupFinished.connect(
            self.__setupFilterCombo, Qt.ConnectionType.QueuedConnection)
        self.__helpFilterCombo.currentIndexChanged.connect(
            self.__filterQtHelpDocumentation)
        self.__helpEngine.filterEngine().filterActivated.connect(
            self.__currentFilterChanged)
        
        self.__setupFilterCombo()
        
        return filterWidget
    
    @pyqtSlot()
    def __setupFilterCombo(self):
        """
        Private slot to setup the filter combo box.
        """
        activeFilter = self.__helpFilterCombo.currentText()
        if not activeFilter:
            activeFilter = self.__helpEngine.filterEngine().activeFilter()
        if not activeFilter:
            activeFilter = self.tr("Unfiltered")
        allFilters = self.__helpEngine.filterEngine().filters()
        
        blocked = self.__helpFilterCombo.blockSignals(True)
        self.__helpFilterCombo.clear()
        self.__helpFilterCombo.addItem(self.tr("Unfiltered"))
        if allFilters:
            self.__helpFilterCombo.insertSeparator(1)
            for helpFilter in sorted(allFilters):
                self.__helpFilterCombo.addItem(helpFilter, helpFilter)
        self.__helpFilterCombo.blockSignals(blocked)
        
        self.__helpFilterCombo.setCurrentText(activeFilter)
    
    @pyqtSlot(int)
    def __filterQtHelpDocumentation(self, index):
        """
        Private slot to filter the QtHelp documentation.
        
        @param index index of the selected QtHelp documentation filter
        @type int
        """
        if self.__helpEngine:
            helpFilter = self.__helpFilterCombo.itemData(index)
            self.__helpEngine.filterEngine().setActiveFilter(helpFilter)
    
    @pyqtSlot(str)
    def __currentFilterChanged(self, filter_):
        """
        Private slot handling a change of the active QtHelp filter.
        
        @param filter_ filter name
        @type str
        """
        index = self.__helpFilterCombo.findData(filter_)
        if index < 0:
            index = 0
        self.__helpFilterCombo.setCurrentIndex(index)
    
    #######################################################################
    ## QWebEngine related code below
    #######################################################################
    
    def __initQWebEngine(self):
        """
        Private method to initialize global QWebEngine related objects.
        """
        self.__webProfile = QWebEngineProfile.defaultProfile()
        self.__webProfile.setHttpCacheType(
            QWebEngineProfile.HttpCacheType.MemoryHttpCache)
        self.__webProfile.setHttpCacheMaximumSize(0)
        
        self.__initQWebEngineSettings()
        
        from WebBrowser.Network.QtHelpSchemeHandler import QtHelpSchemeHandler
        self.__qtHelpSchemeHandler = QtHelpSchemeHandler(self.__helpEngine)
        self.__webProfile.installUrlSchemeHandler(
            QByteArray(b"qthelp"), self.__qtHelpSchemeHandler)
    
    def webProfile(self):
        """
        Public method to get a reference to the global web profile object.
        
        @return reference to the global web profile object
        @rtype QWebEngineProfile
        """
        return self.__webProfile
    
    def webSettings(self):
        """
        Public method to get the web settings of the current profile.
        
        @return web settings of the current profile
        @rtype QWebEngineSettings
        """
        return self.webProfile().settings()
    
    def __initQWebEngineSettings(self):
        """
        Private method to set the global web settings.
        """
        settings = self.webSettings()
        
        settings.setFontFamily(
            QWebEngineSettings.FontFamily.StandardFont,
            Preferences.getWebBrowser("StandardFontFamily"))
        settings.setFontFamily(
            QWebEngineSettings.FontFamily.FixedFont,
            Preferences.getWebBrowser("FixedFontFamily"))
        settings.setFontFamily(
            QWebEngineSettings.FontFamily.SerifFont,
            Preferences.getWebBrowser("SerifFontFamily"))
        settings.setFontFamily(
            QWebEngineSettings.FontFamily.SansSerifFont,
            Preferences.getWebBrowser("SansSerifFontFamily"))
        settings.setFontFamily(
            QWebEngineSettings.FontFamily.CursiveFont,
            Preferences.getWebBrowser("CursiveFontFamily"))
        settings.setFontFamily(
            QWebEngineSettings.FontFamily.FantasyFont,
            Preferences.getWebBrowser("FantasyFontFamily"))
        
        settings.setFontSize(
            QWebEngineSettings.FontSize.DefaultFontSize,
            Preferences.getWebBrowser("DefaultFontSize"))
        settings.setFontSize(
            QWebEngineSettings.FontSize.DefaultFixedFontSize,
            Preferences.getWebBrowser("DefaultFixedFontSize"))
        settings.setFontSize(
            QWebEngineSettings.FontSize.MinimumFontSize,
            Preferences.getWebBrowser("MinimumFontSize"))
        settings.setFontSize(
            QWebEngineSettings.FontSize.MinimumLogicalFontSize,
            Preferences.getWebBrowser("MinimumLogicalFontSize"))
        
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.AutoLoadImages,
            Preferences.getWebBrowser("AutoLoadImages"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.JavascriptEnabled,
            True)
        # JavaScript is needed for the web browser functionality
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows,
            Preferences.getWebBrowser("JavaScriptCanOpenWindows"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard,
            Preferences.getWebBrowser("JavaScriptCanAccessClipboard"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.PluginsEnabled,
            False)
        
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.LocalStorageEnabled,
            False)
        settings.setDefaultTextEncoding(
            Preferences.getWebBrowser("DefaultTextEncoding"))
        
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.SpatialNavigationEnabled,
            Preferences.getWebBrowser("SpatialNavigationEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.LinksIncludedInFocusChain,
            Preferences.getWebBrowser("LinksIncludedInFocusChain"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls,
            Preferences.getWebBrowser("LocalContentCanAccessRemoteUrls"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls,
            Preferences.getWebBrowser("LocalContentCanAccessFileUrls"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.XSSAuditingEnabled,
            Preferences.getWebBrowser("XSSAuditingEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.ScrollAnimatorEnabled,
            Preferences.getWebBrowser("ScrollAnimatorEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.ErrorPageEnabled,
            Preferences.getWebBrowser("ErrorPageEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.FullScreenSupportEnabled,
            False)
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.ScreenCaptureEnabled,
            Preferences.getWebBrowser("ScreenCaptureEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.WebGLEnabled,
            Preferences.getWebBrowser("WebGLEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled,
            Preferences.getWebBrowser("FocusOnNavigationEnabled"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.PrintElementBackgrounds,
            Preferences.getWebBrowser("PrintElementBackgrounds"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.AllowRunningInsecureContent,
            Preferences.getWebBrowser("AllowRunningInsecureContent"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.AllowGeolocationOnInsecureOrigins,
            Preferences.getWebBrowser("AllowGeolocationOnInsecureOrigins"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute
            .AllowWindowActivationFromJavaScript,
            Preferences.getWebBrowser(
                "AllowWindowActivationFromJavaScript"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.ShowScrollBars,
            Preferences.getWebBrowser("ShowScrollBars"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture,
            Preferences.getWebBrowser(
                "PlaybackRequiresUserGesture"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.JavascriptCanPaste,
            Preferences.getWebBrowser(
                "JavaScriptCanPaste"))
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.WebRTCPublicInterfacesOnly,
            False)
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.DnsPrefetchEnabled,
            False)
        settings.setAttribute(
            QWebEngineSettings.WebAttribute.PdfViewerEnabled,
            Preferences.getWebBrowser(
                "PdfViewerEnabled"))
    
    #######################################################################
    ## Search widget related methods below
    #######################################################################
    
    @pyqtSlot()
    def __searchWidgetClosed(self):
        """
        Private slot to handle the closing of the search widget.
        """
        self.__searchButton.setChecked(False)
    
    @pyqtSlot(bool)
    def showHideSearch(self, visible):
        """
        Public slot to show or hide the search widget.
        
        @param visible flag indicating to show or hide the search widget
        @type bool
        """
        self.__searchWidget.setVisible(visible)
        if visible:
            self.__searchWidget.activate()
        else:
            self.__searchWidget.deactivate()
    
    @pyqtSlot()
    def searchPrev(self):
        """
        Public slot to find the previous occurrence of the current search term.
        """
        self.__searchWidget.findPrev()
    
    @pyqtSlot()
    def searchNext(self):
        """
        Public slot to find the next occurrence of the current search term.
        """
        self.__searchWidget.findNext()
    
    #######################################################################
    ## Utility methods below
    #######################################################################
    
    def openPagesCount(self):
        """
        Public method to get the count of open pages.
        
        @return count of open pages
        @rtype int
        """
        return self.__helpStack.count()

eric ide

mercurial