eric7/HelpViewer/HelpViewerWidget.py

Wed, 13 Oct 2021 18:15:30 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 13 Oct 2021 18:15:30 +0200
branch
eric7
changeset 8683
e8a907801549
parent 8681
6285e8374d99
child 8685
b0669ce1066d
permissions
-rw-r--r--

Continued implementing the embedded help viewer widget. Added the QtHelp Table of Contents widget.

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

# Copyright (c) 2021 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
from PyQt6.QtGui import QAction
from PyQt6.QtHelp import QHelpEngine
from PyQt6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout, QComboBox, QSizePolicy, QStackedWidget,
    QToolButton, QButtonGroup, QAbstractButton, QMenu, QFrame
)

from EricWidgets import EricFileDialog, EricMessageBox

import UI.PixmapCache
import Utilities
import Preferences

from .OpenPagesWidget import OpenPagesWidget

from WebBrowser.QtHelp.HelpTocWidget import HelpTocWidget


class HelpViewerWidget(QWidget):
    """
    Class implementing an embedded viewer for QtHelp and local HTML files.
    """
    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)
        
        ###################################################################
        
        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.currentIndexChanged.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)
        
        ###################################################################
        
        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.__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)

        ###################################################################
        
        self.__helpStack = QStackedWidget(self)
        self.__helpStack.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.__layout.addWidget(self.__helpStack)
        
        ###################################################################
        
        self.__helpNavigationStack = QStackedWidget(self)
        self.__helpNavigationStack.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.__helpNavigationStack.setMaximumHeight(200)
        self.__layout.addWidget(self.__helpNavigationStack)
        self.__populateNavigationStack()
        
        ###################################################################
        
        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 table of contents"))
        self.__openPagesButton.setChecked(True)
        
        # TODO: add buttons for the QHelp related widgets
        
        self.__buttonLayout.addStretch()
        
        self.__layout.addLayout(self.__buttonLayout)
        
        ###################################################################
        
        self.setLayout(self.__layout)
        
        self.__openPagesButton.setChecked(True)
        
        self.__initHelpEngine()
        
        self.__ui.preferencesChanged.connect(self.__populateHelpSelector)
        
        self.__initActionsMenu()
        
        self.addPage()
        self.__checkActionButtons()
    
    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.__checkActionButtons)
        self.__helpNavigationStack.addWidget(self.__openPagesList)
        
        # QtHelp TOC
        self.__tocWidget = HelpTocWidget(self.__helpEngine, internal=True)
        self.__tocWidget.escapePressed.connect(self.__activateCurrentPage)
        self.__tocWidget.openUrl.connect(self.openUrl)
        self.__tocWidget.newTab.connect(self.openUrlNewPage)
        self.__tocWidget.newBackgroundTab.connect(
            self.openUrlNewBackgroundPage)
        self.__helpNavigationStack.addWidget(self.__tocWidget)
        
        # TODO: not yet implemented
    
    @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.__tocWidget)
        
        # TODO: not yet implemented
    
    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.currentViewer().setUrl(url)
    
    def searchQtHelp(self, searchExpression):
        """
        Public method to search for a given search expression.
        
        @param searchExpression expression to search for
        @type str
        """
        # TODO: not yet implemented
    
    def activate(self, searchWord=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)
        """
        cv = self.currentViewer()
        if cv:
            cv.setFocus(Qt.FocusReason.OtherFocusReason)
        
        if searchWord:
            self.searchQtHelp(searchWord)
    
    @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().setUrl(QUrl.fromLocalFile(htmlFile))
    
    def addPage(self, url=QUrl("about:blank"), 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)
        """
        viewer = self.__newViewer()
        viewer.setUrl(url)
        
        if background:
            cv = self.currentViewer()
            if cv:
                index = self.__helpStack.indexOf(cv) + 1
                self.__helpStack.insertWidget(index, viewer)
                self.__openPagesList.insertPage(
                    index, viewer, background=background)
                return
        
        self.__helpStack.addWidget(viewer)
        self.__openPagesList.addPage(viewer, background=background)

    @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.setUrl(url)
    
    @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 __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 help viewer
        @rtype HelpViewerImpl
        """
        try:
            from .HelpViewerImpl_qwe import HelpViewerImpl_qwe
            viewer = HelpViewerImpl_qwe(self.__helpEngine, self)
        except ImportError:
            from .HelpViewerImpl_qtb import HelpViewerImpl_qtb
            viewer = HelpViewerImpl_qtb(self.__helpEngine, self)
        
        viewer.zoomChanged.connect(self.__checkActionButtons)
        
        return viewer
    
    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.setupData()
        self.__helpEngine.setUsesFilterEngine(True)
        self.__removeOldDocumentation()
        self.__helpEngine.warning.connect(self.__warning)
    
    def __getQtHelpCollectionFileName(cls):
        """
        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 __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 __reindexDocumentation(self):
        """
        Private slot 
        """
    
    #######################################################################
    ## 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)
        act = self.__actionsMenu.addAction(self.tr("Reindex Documentation"),
                                           self.__reindexDocumentation)
##        act.triggered.connect(self.__searchEngine.reindexDocumentation)
        
        self.__actionsButton.setMenu(self.__actionsMenu)
    
    #######################################################################
    ## 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()
    
    @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)
    
    def __showBackMenu(self):
        """
        Private slot showing the backward navigation menu.
        """
        cv = self.currentViewer()
        if cv:
            self.__backMenu.clear()
            backwardHistoryCount = min(cv.backwardHistoryCount(), 20)
            # show max. 20 items
            
            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(), 20)
            # show max. 20 items
        
            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()
    
    #######################################################################
    ## 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()

eric ide

mercurial