Continued implementing the embedded help viewer widget. Implemented most part of the QWebEngine based help viewer. eric7

Sat, 16 Oct 2021 20:38:23 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 16 Oct 2021 20:38:23 +0200
branch
eric7
changeset 8693
d51660d6f1b9
parent 8692
9c72f9bc9d72
child 8694
ee70b17dcd71

Continued implementing the embedded help viewer widget. Implemented most part of the QWebEngine based help viewer.

eric7.epj file | annotate | diff | comparison | revisions
eric7/HelpViewer/HelpViewerImpl.py file | annotate | diff | comparison | revisions
eric7/HelpViewer/HelpViewerImpl_qtb.py file | annotate | diff | comparison | revisions
eric7/HelpViewer/HelpViewerImpl_qwe.py file | annotate | diff | comparison | revisions
eric7/HelpViewer/HelpViewerWidget.py file | annotate | diff | comparison | revisions
eric7/WebBrowser/QtHelp/HelpIndexWidget.py file | annotate | diff | comparison | revisions
eric7/WebBrowser/QtHelp/HelpSearchWidget.py file | annotate | diff | comparison | revisions
eric7/WebBrowser/QtHelp/HelpTocWidget.py file | annotate | diff | comparison | revisions
eric7/eric7.py file | annotate | diff | comparison | revisions
--- a/eric7.epj	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7.epj	Sat Oct 16 20:38:23 2021 +0200
@@ -2284,7 +2284,8 @@
       "eric7/HelpViewer/HelpViewerWidget.py",
       "eric7/HelpViewer/OpenPagesWidget.py",
       "eric7/HelpViewer/HelpViewerImpl.py",
-      "eric7/HelpViewer/HelpViewerImpl_qtb.py"
+      "eric7/HelpViewer/HelpViewerImpl_qtb.py",
+      "eric7/HelpViewer/HelpViewerImpl_qwe.py"
     ],
     "SPELLEXCLUDES": "Dictionaries/excludes.dic",
     "SPELLLANGUAGE": "en_US",
--- a/eric7/HelpViewer/HelpViewerImpl.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/HelpViewer/HelpViewerImpl.py	Sat Oct 16 20:38:23 2021 +0200
@@ -123,6 +123,26 @@
         """
         raise RuntimeError("Not implemented")
     
+    def isBackwardAvailable(self):
+        """
+        Public method to check, if stepping backward through the history is
+        available.
+        
+        @exception RuntimeError raised when not implemented
+        """
+        raise RuntimeError("Not implemented")
+        return False
+    
+    def isForwardAvailable(self):
+        """
+        Public method to check, if stepping forward through the history is
+        available.
+        
+        @exception RuntimeError raised when not implemented
+        """
+        raise RuntimeError("Not implemented")
+        return False
+    
     def scaleUp(self):
         """
         Public method to zoom in.
@@ -139,6 +159,16 @@
         """
         raise RuntimeError("Not implemented")
     
+    def setScale(self, scale):
+        """
+        Public method to set the zoom level.
+        
+        @param scale zoom level to set
+        @type int
+        @exception RuntimeError raised when not implemented
+        """
+        raise RuntimeError("Not implemented")
+    
     def resetScale(self):
         """
         Public method to reset the zoom level.
--- a/eric7/HelpViewer/HelpViewerImpl_qtb.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/HelpViewer/HelpViewerImpl_qtb.py	Sat Oct 16 20:38:23 2021 +0200
@@ -82,7 +82,19 @@
         @return page title
         @rtype str
         """
-        return self.documentTitle()
+        titleStr = self.documentTitle()
+        if not titleStr:
+            url = self.url()
+            
+            titleStr = url.host()
+            if not titleStr:
+                titleStr = url.toString(
+                    QUrl.UrlFormattingOption.RemoveFragment)
+        
+        if not titleStr or titleStr == "about:blank":
+            titleStr = self.tr("Empty Page")
+        
+        return titleStr
     
     def loadResource(self, type_,  name):
         """
@@ -137,6 +149,20 @@
             for ind in range(index):
                 self.forward()
     
+    def isBackwardAvailable(self):
+        """
+        Public method to check, if stepping backward through the history is
+        available.
+        """
+        return QTextBrowser.isBackwardAvailable(self)
+    
+    def isForwardAvailable(self):
+        """
+        Public method to check, if stepping forward through the history is
+        available.
+        """
+        return QTextBrowser.isForwardAvailable(self)
+    
     def scaleUp(self):
         """
         Public method to zoom in.
@@ -210,12 +236,21 @@
         @param evt reference to the event object
         @type QWheelEvent
         """
+        delta = evt.angleDelta().y()
         if evt.modifiers() == Qt.KeyboardModifier.ControlModifier:
-            if evt.angleDelta().y() > 0:
+            if delta > 0:
                 self.scaleUp()
             else:
                 self.scaleDown()
             evt.accept()
+        
+        elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier:
+            if delta < 0:
+                self.backward()
+            elif delta > 0:
+                self.forward()
+            evt.accept()
+        
         else:
             QTextBrowser.wheelEvent(self, evt)
     
@@ -287,10 +322,13 @@
             evt.accept()
     
     # TODO: implement context menu
+    #       - Backward
+    #       - Forward
+    #       - Reload
     #       - Open Link
     #       - Open Link in New Page
     #       - Open Link in Background Page
     #       - Copy
-    #       - Copy Link LOcation
+    #       - Copy Link Location
     #       - Select All
     # TODO: add Ctrl+LMB action (open link in new page)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/HelpViewer/HelpViewerImpl_qwe.py	Sat Oct 16 20:38:23 2021 +0200
@@ -0,0 +1,688 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the help viewer base class.
+"""
+
+from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QTimer, QUrl, QPoint
+from PyQt6.QtGui import QGuiApplication, QClipboard, QContextMenuEvent
+from PyQt6.QtWidgets import QMenu
+from PyQt6.QtWebEngineWidgets import QWebEngineView
+from PyQt6.QtWebEngineCore import QWebEnginePage
+
+from .HelpViewerWidget import HelpViewerWidget
+from .HelpViewerImpl import HelpViewerImpl
+
+import UI.PixmapCache
+
+class HelpViewerImpl_qwe(HelpViewerImpl, QWebEngineView):
+    """
+    Class implementing the QTextBrowser based help viewer class.
+    """
+    ZoomLevels = [
+        30, 40, 50, 67, 80, 90,
+        100,
+        110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300,
+    ]
+    ZoomLevelDefault = 100
+    
+    def __init__(self, engine, parent=None):
+        """
+        Constructor
+        
+        @param engine reference to the help engine
+        @type QHelpEngine
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        QWebEngineView.__init__(self, parent=parent)
+        HelpViewerImpl.__init__(self, engine)
+        
+        self.__helpViewerWidget = parent
+        
+        self.__rwhvqt = None
+        self.installEventFilter(self)
+        
+        self.__page = None
+        self.__createNewPage()
+        
+        self.__currentScale = 100
+        
+        self.__menu = QMenu(self)
+    
+    def __createNewPage(self):
+        """
+        Private method to create a new page object.
+        """
+        self.__page = QWebEnginePage(self.__helpViewerWidget.webProfile())
+        self.setPage(self.__page)
+        
+        self.__page.titleChanged.connect(self.__titleChanged)
+        self.__page.urlChanged.connect(self.__titleChanged)
+    
+    def __setRwhvqt(self):
+        """
+        Private slot to set widget that receives input events.
+        """
+        self.grabGesture(Qt.GestureType.PinchGesture)
+        self.__rwhvqt = self.focusProxy()
+        if self.__rwhvqt:
+            self.__rwhvqt.grabGesture(Qt.GestureType.PinchGesture)
+            self.__rwhvqt.installEventFilter(self)
+        else:
+            print("Focus proxy is null!")   # __IGNORE_WARNING_M801__
+    
+    def setUrl(self, url):
+        """
+        Public method to set the URL of the document to be shown.
+        
+        @param url URL of the document
+        @type QUrl
+        """
+        QWebEngineView.setUrl(self, url)
+    
+    def url(self):
+        """
+        Public method to get the URL of the shown document.
+        
+        @return url URL of the document
+        @rtype QUrl
+        """
+        return QWebEngineView.url(self)
+    
+    @pyqtSlot()
+    def __titleChanged(self):
+        """
+        Private method to handle a change of the web page title.
+        """
+        super().titleChanged.emit()
+    
+    def title(self):
+        """
+        Public method get the page title.
+        
+        @return page title
+        @rtype str
+        """
+        titleStr = QWebEngineView.title(self)
+        if not titleStr:
+            if self.url().isEmpty():
+                url = self.__page.requestedUrl()
+            else:
+                url = self.url()
+            
+            titleStr = url.host()
+            if not titleStr:
+                titleStr = url.toString(
+                    QUrl.UrlFormattingOption.RemoveFragment)
+        
+        if not titleStr or titleStr == "about:blank":
+            titleStr = self.tr("Empty Page")
+        
+        return titleStr
+    
+    def isBackwardAvailable(self):
+        """
+        Public method to check, if stepping backward through the history is
+        available.
+        """
+        return self.history().canGoBack()
+    
+    def isForwardAvailable(self):
+        """
+        Public method to check, if stepping forward through the history is
+        available.
+        """
+        return self.history().canGoForward()
+    
+    def backward(self):
+        """
+        Public slot to move backwards in history.
+        """
+        self.triggerPageAction(QWebEnginePage.WebAction.Back)
+    
+    def forward(self):
+        """
+        Public slot to move forward in history.
+        """
+        self.triggerPageAction(QWebEnginePage.WebAction.Forward)
+    
+    def reload(self):
+        """
+        Public slot to reload the current page.
+        """
+        self.triggerPageAction(QWebEnginePage.WebAction.Reload)
+    
+    def backwardHistoryCount(self):
+        """
+        Public method to get the number of available back history items.
+        
+        Note: For performance reasons this is limited to the maximum number of
+        history items the help viewer is interested in.
+        
+        @return count of available back history items
+        @rtype int
+        """
+        history = self.history()
+        return len(history.backItems(HelpViewerWidget.MaxHistoryItems))
+    
+    def forwardHistoryCount(self):
+        """
+        Public method to get the number of available forward history items.
+        
+        Note: For performance reasons this is limited to the maximum number of
+        history items the help viewer is interested in.
+        
+        @return count of available forward history items
+        @rtype int
+        """
+        history = self.history()
+        return len(history.forwardItems(HelpViewerWidget.MaxHistoryItems))
+    
+    def historyTitle(self, offset):
+        """
+        Public method to get the title of a history item.
+        
+        @param offset offset of the item with respect to the current page
+        @type int
+        @return title of the requeted item in history
+        @rtype str
+        """
+        history = self.history()
+        currentIndex = history.currentItemIndex()
+        itm = self.history().itemAt(currentIndex + offset)
+        return itm.title()
+    
+    def gotoHistory(self, offset):
+        """
+        Public method to go to ahistory item
+        
+        @param offset offset of the item with respect to the current page
+        @type int
+        """
+        history = self.history()
+        currentIndex = history.currentItemIndex()
+        itm = self.history().itemAt(currentIndex + offset)
+        history.goToItem(itm)
+    
+    def clearHistory(self):
+        """
+        Public method to clear the history.
+        """
+        self.history().clear()
+    
+    #######################################################################
+    
+    def __levelForScale(self, scale):
+        """
+        Private method determining the zoom level index given a zoom factor.
+        
+        @param zoom zoom factor
+        @type int
+        @return index of zoom factor
+        @rtype int
+        """
+        try:
+            index = self.ZoomLevels.index(scale)
+        except ValueError:
+            for index in range(len(self.ZoomLevels)):
+                if scale <= self.ZoomLevels[scale]:
+                    break
+        return index
+    
+    def scaleUp(self):
+        """
+        Public method to zoom in.
+        """
+        index = self.__levelForScale(self.__currentScale)
+        if index < len(self.ZoomLevels) - 1:
+            self.setScale(self.ZoomLevels[index + 1])
+    
+    def scaleDown(self):
+        """
+        Public method to zoom out.
+        """
+        index = self.__levelForScale(self.__currentScale)
+        if index > 0:
+            self.setScale(self.ZoomLevels[index - 1])
+    
+    def setScale(self, scale):
+        """
+        Public method to set the zoom level.
+        
+        @param scale zoom level to set
+        @type int
+        """
+        if scale != self.__currentScale:
+            self.setZoomFactor(scale / 100.0)
+            self.__currentScale = scale
+            self.zoomChanged.emit()
+    
+    def resetScale(self):
+        """
+        Public method to reset the zoom level.
+        """
+        index = self.__levelForScale(self.ZoomLevelDefault)
+        self.setScale(self.ZoomLevels[index])
+    
+    def scale(self):
+        """
+        Public method to get the zoom level.
+        
+        @return current zoom level
+        @rtype int
+        """
+        return self.__currentScale
+    
+    def isScaleUpAvailable(self):
+        """
+        Public method to check, if the max. zoom level is reached.
+        
+        @return flag indicating scale up is available
+        @rtype bool
+        """
+        index = self.__levelForScale(self.__currentScale)
+        return index < len(self.ZoomLevels) - 1
+    
+    def isScaleDownAvailable(self):
+        """
+        Public method to check, if the min. zoom level is reached.
+        
+        @return flag indicating scale down is available
+        @rtype bool
+        """
+        index = self.__levelForScale(self.__currentScale)
+        return index > 0
+    
+    #######################################################################
+    ## Event handlers below
+    #######################################################################
+    
+    def eventFilter(self, obj, evt):
+        """
+        Public method to process event for other objects.
+        
+        @param obj reference to object to process events for
+        @type QObject
+        @param evt reference to event to be processed
+        @type QEvent
+        @return flag indicating that the event should be filtered out
+        @rtype bool
+        """
+        if (
+            obj is self and
+            evt.type() == QEvent.Type.ParentChange and
+            self.parentWidget() is not None
+        ):
+            self.parentWidget().installEventFilter(self)
+        
+        # find the render widget receiving events for the web page
+        if obj is self and evt.type() == QEvent.Type.ChildAdded:
+            QTimer.singleShot(0, self.__setRwhvqt)
+        
+        # forward events to WebBrowserView
+        if (
+            obj is self.__rwhvqt and
+            evt.type() in [QEvent.Type.KeyPress,
+                           QEvent.Type.MouseButtonRelease,
+                           QEvent.Type.Wheel,
+                           QEvent.Type.Gesture]
+        ):
+            wasAccepted = evt.isAccepted()
+            evt.setAccepted(False)
+            if evt.type() == QEvent.Type.KeyPress:
+                self._keyPressEvent(evt)
+            elif evt.type() == QEvent.Type.MouseButtonRelease:
+                self._mouseReleaseEvent(evt)
+            elif evt.type() == QEvent.Type.Wheel:
+                self._wheelEvent(evt)
+            elif evt.type() == QEvent.Type.Gesture:
+                self._gestureEvent(evt)
+            ret = evt.isAccepted()
+            evt.setAccepted(wasAccepted)
+            return ret
+        
+        if (
+            obj is self.parentWidget() and
+            evt.type() in [QEvent.Type.KeyPress, QEvent.Type.KeyRelease]
+        ):
+            wasAccepted = evt.isAccepted()
+            evt.setAccepted(False)
+            if evt.type() == QEvent.Type.KeyPress:
+                self._keyPressEvent(evt)
+            ret = evt.isAccepted()
+            evt.setAccepted(wasAccepted)
+            return ret
+        
+        # block already handled events
+        if obj is self:
+            if evt.type() in [QEvent.Type.KeyPress,
+                              QEvent.Type.MouseButtonRelease,
+                              QEvent.Type.Wheel,
+                              QEvent.Type.Gesture]:
+                return True
+        
+        return super().eventFilter(obj, evt)
+    
+    def _keyPressEvent(self, evt):
+        """
+        Protected method called by a key press.
+        
+        @param evt reference to the key event
+        @type QKeyEvent
+        """
+        if evt.key() == Qt.Key.Key_ZoomIn:
+            self.scaleUp()
+            evt.accept()
+        elif evt.key() == Qt.Key.Key_ZoomOut:
+            self.scaleDown()
+            evt.accept()
+        elif evt.key() == Qt.Key.Key_Plus:
+            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
+                self.scaleUp()
+                evt.accept()
+        elif evt.key() == Qt.Key.Key_Minus:
+            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
+                self.scaleDown()
+                evt.accept()
+        elif evt.key() == Qt.Key.Key_0:
+            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
+                self.resetScale()
+                evt.accept()
+        elif evt.key() == Qt.Key.Key_Backspace:
+            self.pageAction(QWebEnginePage.WebAction.Back).trigger()
+            evt.accept()
+    
+    def _mouseReleaseEvent(self, evt):
+        """
+        Protected method called by a mouse release event.
+        
+        @param evt reference to the mouse event
+        @type QMouseEvent
+        """
+        accepted = evt.isAccepted()
+        self.__page.event(evt)
+        if (
+            not evt.isAccepted() and
+            evt.button() == Qt.MouseButton.MiddleButton
+        ):
+            url = QUrl(QGuiApplication.clipboard().text(
+                QClipboard.Mode.Selection))
+            if (
+                not url.isEmpty() and
+                url.isValid() and
+                url.scheme() != ""
+            ):
+                self.setUrl(url)
+                accepted = True
+        evt.setAccepted(accepted)
+    
+    def _wheelEvent(self, evt):
+        """
+        Protected method to handle wheel events.
+        
+        @param evt reference to the wheel event
+        @type QWheelEvent
+        """
+        delta = evt.angleDelta().y()
+        if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
+            if delta < 0:
+                self.scaleDown()
+            elif delta > 0:
+                self.scaleUp()
+            evt.accept()
+        
+        elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier:
+            if delta < 0:
+                self.backward()
+            elif delta > 0:
+                self.forward()
+            evt.accept()
+    
+    def _gestureEvent(self, evt):
+        """
+        Protected method handling gesture events.
+        
+        @param evt reference to the gesture event
+        @type QGestureEvent
+        """
+        pinch = evt.gesture(Qt.GestureType.PinchGesture)
+        if pinch:
+            if pinch.state() == Qt.GestureState.GestureStarted:
+                pinch.setTotalScaleFactor(self.__currentScale / 100.0)
+            elif pinch.state() == Qt.GestureState.GestureUpdated:
+                scaleFactor = pinch.totalScaleFactor()
+                self.setScale(int(scaleFactor * 100))
+            evt.accept()
+    
+    def event(self, evt):
+        """
+        Public method handling events.
+        
+        @param evt reference to the event (QEvent)
+        @return flag indicating, if the event was handled (boolean)
+        """
+        if evt.type() == QEvent.Type.Gesture:
+            self._gestureEvent(evt)
+            return True
+        
+        return super().event(evt)
+    
+    #######################################################################
+    ## Context menu related methods below
+    #######################################################################
+    # TODO: implement context menu
+    
+    def contextMenuEvent(self, evt):
+        """
+        Protected method called to create a context menu.
+        
+        This method is overridden from QWebEngineView.
+        
+        @param evt reference to the context menu event object
+        @type QContextMenuEvent
+        """
+        pos = evt.pos()
+        reason = evt.reason()
+        QTimer.singleShot(
+            0,
+            lambda: self._contextMenuEvent(QContextMenuEvent(reason, pos)))
+        # needs to be done this way because contextMenuEvent is blocking
+        # the main loop
+    
+    def _contextMenuEvent(self, evt):
+        """
+        Protected method called to create a context menu.
+        
+        @param evt reference to the context menu event object
+            (QContextMenuEvent)
+        """
+        self.__menu.clear()
+        
+        self.__createContextMenu(self.__menu)
+        
+        if not self.__menu.isEmpty():
+            pos = evt.globalPos()
+            self.__menu.popup(QPoint(pos.x(), pos.y() + 1))
+    
+    def __createContextMenu(self, menu):
+        """
+        Private method to populate the context menu.
+        
+        @param menu reference to the menu to be populated
+        @type QMenu
+        """
+        contextMenuData = self.lastContextMenuRequest()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("back"),
+            self.tr("Backward"),
+            self.backward)
+        act.setEnabled(self.isBackwardAvailable())
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("forward"),
+            self.tr("Forward"),
+            self.forward)
+        act.setEnabled(self.isForwardAvailable())
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("reload"),
+            self.tr("Reload"),
+            self.reload)
+        
+        if (
+            not contextMenuData.linkUrl().isEmpty() and
+            contextMenuData.linkUrl().scheme() != "javascript"
+        ):
+            self.__createLinkContextMenu(menu, contextMenuData)
+        
+        menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy Page URL to Clipboard"))
+        act.setData(self.url())
+        act.triggered.connect(
+            lambda: self.__copyLink(act))
+        
+        menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("zoomIn"),
+            self.tr("Zoom in"),
+            self.scaleUp)
+        act.setEnabled(self.isScaleUpAvailable)
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("zoomOut"),
+            self.tr("Zoom out"),
+            self.scaleDown)
+        act.setEnabled(self.isScaleDownAvailable())
+        
+        menu.addAction(
+            UI.PixmapCache.getIcon("zoomReset"),
+            self.tr("Zoom reset"),
+            self.resetScale)
+        
+        menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy"))
+        act.setData(contextMenuData.selectedText())
+        act.triggered.connect( lambda: self.__copyText(act))
+        act.setEnabled(bool(contextMenuData.selectedText()))
+        
+        menu.addAction(
+            UI.PixmapCache.getIcon("editSelectAll"),
+            self.tr("Select All"),
+            self.__selectAll)
+        
+        menu.addSeparator()
+        
+        menu.addAction(
+            UI.PixmapCache.getIcon("tabClose"),
+            self.tr('Close'),
+            self.__closePage)
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("tabCloseOther"),
+            self.tr("Close Others"),
+            self.__closeOtherPages)
+        act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1)
+    
+    def __createLinkContextMenu(self, menu, contextMenuData):
+        """
+        Private method to populate the context menu for URLs.
+        
+        @param menu reference to the menu to be populated
+        @type QMenu
+        @param contextMenuData data of the last context menu request
+        @type QWebEngineContextMenuRequest
+        """
+        if not menu.isEmpty():
+            menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("openNewTab"),
+            self.tr("Open Link in New Page"))
+        act.setData(contextMenuData.linkUrl())
+        act.triggered.connect(
+            lambda: self.__openLinkInNewPage(act))
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("newWindow"),
+            self.tr("Open Link in Background Page"))
+        act.setData(contextMenuData.linkUrl())
+        act.triggered.connect(
+            lambda: self.__openLinkInBackgroundPage(act))
+        
+        menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy URL to Clipboard"))
+        act.setData(contextMenuData.linkUrl())
+        act.triggered.connect(
+            lambda: self.__copyLink(act))
+    
+    def __openLinkInNewPage(self, act):
+        """
+        Private method called by the context menu to open a link in a new page.
+        
+        @param act reference to the action that triggered
+        @type QAction
+        """
+        # TODO: not yet implemented
+    
+    def __openLinkInBackgroundPage(self, act):
+        """
+        Private method called by the context menu to open a link in a
+        background page.
+        
+        @param act reference to the action that triggered
+        @type QAction
+        """
+        # TODO: not yet implemented
+    
+    def __copyLink(self, act):
+        """
+        Private method called by the context menu to copy a link to the
+        clipboard.
+        
+        @param act reference to the action that triggered
+        @type QAction
+        """
+        # TODO: not yet implemented
+    
+    def __copyText(self, act):
+        """
+        Private method called by the context menu to copy selected text to the
+        clipboard.
+        
+        @param act reference to the action that triggered
+        @type QAction
+        """
+        # TODO: not yet implemented
+    
+    def __selectAll(self):
+        """
+        Private method called by the context menu to select all text.
+        """
+        # TODO: not yet implemented
+    
+    def __closePage(self):
+        """
+        Private method called by the context menu to close the current page.
+        """
+        # TODO: not yet implemented
+    
+    def __closeOtherPages(self):
+        """
+        Private method called by the context menu to close all other pages.
+        """
+        # TODO: not yet implemented
--- a/eric7/HelpViewer/HelpViewerWidget.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/HelpViewer/HelpViewerWidget.py	Sat Oct 16 20:38:23 2021 +0200
@@ -9,7 +9,7 @@
 
 import os
 
-from PyQt6.QtCore import pyqtSlot, Qt, QUrl, QTimer
+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 (
@@ -17,6 +17,11 @@
     QToolButton, QButtonGroup, QAbstractButton, QMenu, QFrame, QLabel,
     QProgressBar
 )
+try:
+    from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings
+    WEBENGINE_AVAILABLE = True
+except ImportError:
+    WEBENGINE_AVAILABLE = True
 
 from EricWidgets import EricFileDialog, EricMessageBox
 
@@ -35,6 +40,8 @@
     """
     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
@@ -62,8 +69,7 @@
             QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
         self.__selectorLayout.addWidget(self.__helpSelector)
         self.__populateHelpSelector()
-        self.__helpSelector.currentIndexChanged.connect(
-            self.__helpTopicSelected)
+        self.__helpSelector.activated.connect(self.__helpTopicSelected)
         
         self.__openButton = QToolButton(self)
         self.__openButton.setIcon(UI.PixmapCache.getIcon("open"))
@@ -137,6 +143,14 @@
         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)
+        
+        # TODO: add plus button to open a new page (about:blank)
+        # TODO: add minus button to close the current page
+        
         self.__navButtonsLayout.addStretch()
         
         self.__layout.addLayout(self.__navButtonsLayout)
@@ -161,6 +175,10 @@
         
         ###################################################################
         
+        # TODO: addd a search widget (EricTextEditSearchWidget)
+        
+        ###################################################################
+        
         self.__helpNavigationStack = QStackedWidget(self)
         self.__helpNavigationStack.setSizePolicy(
             QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
@@ -210,6 +228,10 @@
         
         self.__initActionsMenu()
         
+        if WEBENGINE_AVAILABLE:
+            self.__initQWebEngine()
+            self.__ui.preferencesChanged.connect(self.__initQWebEngineSettings)
+        
         self.addPage()
         self.__checkActionButtons()
         
@@ -241,7 +263,7 @@
         """
         # Open Pages
         self.__openPagesList = OpenPagesWidget(self.__helpStack, self)
-        self.__openPagesList.currentChanged.connect(self.__checkActionButtons)
+        self.__openPagesList.currentChanged.connect(self.__currentPageChanged)
         self.__helpNavigationStack.addWidget(self.__openPagesList)
         
         # QtHelp TOC widget
@@ -335,7 +357,7 @@
         urlStr = self.__helpSelector.currentData()
         if urlStr:
             url = QUrl(urlStr)
-            self.currentViewer().setUrl(url)
+            self.openUrl(url)
     
     def activate(self, searchWord=None):
         """
@@ -394,11 +416,13 @@
                 self.__helpStack.insertWidget(index, viewer)
                 self.__openPagesList.insertPage(
                     index, viewer, background=background)
+                cv.setFocus(Qt.FocusReason.OtherFocusReason)
                 return
         
         self.__helpStack.addWidget(viewer)
         self.__openPagesList.addPage(viewer, background=background)
-
+        viewer.setFocus(Qt.FocusReason.OtherFocusReason)
+    
     @pyqtSlot(QUrl)
     def openUrl(self, url):
         """
@@ -410,6 +434,7 @@
         cv = self.currentViewer()
         if cv:
             cv.setUrl(url)
+            cv.setFocus(Qt.FocusReason.OtherFocusReason)
     
     @pyqtSlot(QUrl)
     def openUrlNewPage(self, url):
@@ -447,10 +472,10 @@
         @return help viewer
         @rtype HelpViewerImpl
         """
-        try:
+        if WEBENGINE_AVAILABLE:
             from .HelpViewerImpl_qwe import HelpViewerImpl_qwe
             viewer = HelpViewerImpl_qwe(self.__helpEngine, self)
-        except ImportError:
+        else:
             from .HelpViewerImpl_qtb import HelpViewerImpl_qtb
             viewer = HelpViewerImpl_qtb(self.__helpEngine, self)
         
@@ -624,23 +649,6 @@
         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.
@@ -648,8 +656,8 @@
         cv = self.currentViewer()
         if cv:
             self.__backMenu.clear()
-            backwardHistoryCount = min(cv.backwardHistoryCount(), 20)
-            # show max. 20 items
+            backwardHistoryCount = min(cv.backwardHistoryCount(),
+                                       HelpViewerWidget.MaxHistoryItems)
             
             for index in range(1, backwardHistoryCount + 1):
                 act = QAction(self)
@@ -668,8 +676,8 @@
         cv = self.currentViewer()
         if cv:
             self.__forwardMenu.clear()
-            forwardHistoryCount = min(cv.forwardHistoryCount(), 20)
-            # show max. 20 items
+            forwardHistoryCount = min(cv.forwardHistoryCount(),
+                                      HelpViewerWidget.MaxHistoryItems)
         
             for index in range(1, forwardHistoryCount + 1):
                 act = QAction(self)
@@ -704,6 +712,37 @@
             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:
+            cv.setFocus(Qt.FocusReason.OtherFocusReason)
+    
+    #######################################################################
     ## Zoom related methods below
     #######################################################################
     
@@ -878,3 +917,184 @@
         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"))
+    
+    #######################################################################
+    ## 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()
--- a/eric7/WebBrowser/QtHelp/HelpIndexWidget.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/WebBrowser/QtHelp/HelpIndexWidget.py	Sat Oct 16 20:38:23 2021 +0200
@@ -241,6 +241,7 @@
                 newBackgroundTab = menu.addAction(
                     self.tr("Open Link in Background Tab"))
                 newWindow = menu.addAction(self.tr("Open Link in New Window"))
+            # TODO: add link to copy the URL
             menu.move(self.__index.mapToGlobal(pos))
             
             act = menu.exec()
--- a/eric7/WebBrowser/QtHelp/HelpSearchWidget.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/WebBrowser/QtHelp/HelpSearchWidget.py	Sat Oct 16 20:38:23 2021 +0200
@@ -170,6 +170,7 @@
             newBackgroundTab = menu.addAction(
                 self.tr("Open Link in Background Tab"))
             newWindow = menu.addAction(self.tr("Open Link in New Window"))
+        # TODO: add link to copy the URL
         menu.move(evt.globalPos())
         
         act = menu.exec()
--- a/eric7/WebBrowser/QtHelp/HelpTocWidget.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/WebBrowser/QtHelp/HelpTocWidget.py	Sat Oct 16 20:38:23 2021 +0200
@@ -183,6 +183,7 @@
             newBackgroundTab = menu.addAction(
                 self.tr("Open Link in Background Tab"))
             newWindow = menu.addAction(self.tr("Open Link in New Window"))
+        # TODO: add link to copy the URL
         menu.move(self.__tocWidget.mapToGlobal(pos))
         
         act = menu.exec()
--- a/eric7/eric7.py	Sat Oct 16 20:37:32 2021 +0200
+++ b/eric7/eric7.py	Sat Oct 16 20:38:23 2021 +0200
@@ -42,9 +42,12 @@
         " it is installed and accessible.")
     sys.exit(100)
 
-with contextlib.suppress(ImportError):
-    from PyQt6 import QtWebEngineWidgets
-    # __IGNORE_WARNING__ __IGNORE_EXCEPTION__
+try:
+    from PyQt6 import QtWebEngineWidgets    # __IGNORE_WARNING__
+    from PyQt6.QtWebEngineCore import QWebEngineUrlScheme
+    WEBENGINE_AVAILABLE = True
+except ImportError:
+    WEBENGINE_AVAILABLE = False
 
 # some global variables needed to start the application
 args = None
@@ -292,6 +295,12 @@
     # set the library paths for plugins
     Startup.setLibraryPaths()
 
+    if WEBENGINE_AVAILABLE:
+        scheme = QWebEngineUrlScheme(b"qthelp")
+        scheme.setSyntax(QWebEngineUrlScheme.Syntax.Path)
+        scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme)
+        QWebEngineUrlScheme.registerScheme(scheme)
+    
     app = EricApplication(sys.argv)
     ddindex = Startup.handleArgs(sys.argv, appinfo)
     

eric ide

mercurial