eric7/HelpViewer/HelpViewerImplQTB.py

branch
eric7
changeset 8696
7e88f292b1b1
parent 8693
d51660d6f1b9
child 8697
936662560d04
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/HelpViewer/HelpViewerImplQTB.py	Sun Oct 17 15:26:31 2021 +0200
@@ -0,0 +1,615 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the QTextBrowser based help viewer class.
+"""
+
+import functools
+
+from PyQt6.QtCore import (
+    pyqtSlot, Qt, QByteArray, QUrl, QEvent, QCoreApplication, QPoint
+)
+from PyQt6.QtGui import QDesktopServices, QImage, QGuiApplication, QClipboard
+from PyQt6.QtWidgets import QTextBrowser, QMenu
+
+from .HelpViewerImpl import HelpViewerImpl
+
+import UI.PixmapCache
+
+
+AboutBlank = QCoreApplication.translate(
+    "HelpViewer",
+    "<html>"
+    "<head><title>about:blank</title></head>"
+    "<body></body>"
+    "</html>")
+
+PageNotFound = QCoreApplication.translate(
+    "HelpViewer",
+    """<html>"""
+    """<head><title>Error 404...</title></head>"""
+    """<body><div align="center"><br><br>"""
+    """<h1>The page could not be found</h1><br>"""
+    """<h3>'{0}'</h3></div></body>"""
+    """</html>""")
+
+
+class HelpViewerImplQTB(HelpViewerImpl, QTextBrowser):
+    """
+    Class implementing the QTextBrowser based help viewer class.
+    """
+    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
+        """
+        QTextBrowser.__init__(self, parent=parent)
+        HelpViewerImpl.__init__(self, engine)
+        
+        self.__helpViewerWidget = parent
+        
+        self.__zoomCount = 0
+        
+        self.__menu = QMenu(self)
+        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+        self.customContextMenuRequested.connect(self.__showContextMenu)
+        
+        self.sourceChanged.connect(self.titleChanged)
+        
+        self.grabGesture(Qt.GestureType.PinchGesture)
+    
+    def setLink(self, url):
+        """
+        Public method to set the URL of the document to be shown.
+        
+        @param url source of the document
+        @type QUrl
+        """
+        self.setSource(url)
+    
+    def link(self):
+        """
+        Public method to get the URL of the shown document.
+        
+        @return URL of the document
+        @rtype QUrl
+        """
+        return self.source()
+    
+    def doSetSource(self, url, type_):
+        """
+        Public method to load the data and show it.
+        
+        @param url URL of resource to load
+        @type QUrl
+        @param type_ type of the resource to load
+        @type QTextDocument.ResourceType
+        """
+        data = self.__getData(url)
+        if data is None:
+            QDesktopServices.openUrl(url)
+            return
+        
+        if url != QUrl("about:blank"):
+            super().doSetSource(url, type)
+        
+        self.setHtml(data)
+        self.sourceChanged.emit(url)
+        self.loadFinished.emit(True)
+    
+    def __getData(self, url):
+        """
+        Private method to get the data to be shown.
+        
+        @param url URL to be loaded
+        @type QUrl
+        @return data to be shown
+        @rtype str
+        """
+        scheme = url.scheme()
+        if scheme == "about":
+            if url.toString() == "about:blank":
+                return AboutBlank
+            else:
+                return PageNotFound.format(url.toString())
+        elif scheme in ("file", ""):
+            filePath = url.toLocalFile()
+            try:
+                with open(filePath, "r", encoding="utf-8") as f:
+                    htmlText = f.read()
+                return htmlText
+            except OSError:
+                return PageNotFound.format(url.toString())
+        elif scheme == "qthelp":
+            if self._engine.findFile(url).isValid():
+                data = bytes(self._engine.fileData(url)).decode("utf-8")
+                if not data:
+                    data = PageNotFound.format(url.toString())
+                return data
+            else:
+                return PageNotFound.format(url.toString())
+        else:
+            # None is an indicator that we cannot handle the request
+            return None
+    
+    def pageTitle(self):
+        """
+        Public method get the page title.
+        
+        @return page title
+        @rtype str
+        """
+        titleStr = self.documentTitle()
+        if not titleStr:
+            url = self.link()
+            
+            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):
+        """
+        Public method to load data of the specified type from the resource with
+        the given name.
+        
+        @param type_ resource type
+        @type int
+        @param name resource name
+        @type QUrl
+        @return byte array containing the loaded data
+        @rtype QByteArray
+        """
+        ba = QByteArray()
+        
+        if type_ < 4:     # QTextDocument.ResourceType.MarkdownResource
+            # TODO: change to use getData()
+            url = self._engine.findFile(name)
+            ba = self._engine.fileData(url)
+            if url.toString().lower().endswith(".svg"):
+                image = QImage()
+                image.loadFromData(ba, "svg")
+                if not image.isNull():
+                    return image
+        
+        return ba
+    
+    def mousePressEvent(self, evt):
+        """
+        Protected method called by a mouse press event.
+        
+        @param evt reference to the mouse event
+        @type QMouseEvent
+        """
+        if evt.button() == Qt.MouseButton.XButton1:
+            self.backward()
+            evt.accept()
+        elif evt.button() == Qt.MouseButton.XButton2:
+            self.forward()
+            evt.accept()
+        else:
+            super().mousePressEvent(evt)
+    
+    def mouseReleaseEvent(self, evt):
+        """
+        Protected method called by a mouse release event.
+        
+        @param evt reference to the mouse event
+        @type QMouseEvent
+        """
+        hasModifier = evt.modifiers() != Qt.KeyboardModifier.NoModifier
+        if evt.button() == Qt.MouseButton.LeftButton and hasModifier:
+            
+            anchor = self.anchorAt(evt.pos())
+            if anchor:
+                url = self.link().resolved(QUrl(anchor))
+                if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
+                    self.__helpViewerWidget.openUrlNewBackgroundPage(url)
+                else:
+                    self.__helpViewerWidget.openUrlNewPage(url)
+                evt.accept()
+        else:
+            super().mousePressEvent(evt)
+    
+    def gotoHistory(self, index):
+        """
+        Public method to step through the history.
+        
+        @param index history index (<0 backward, >0 forward)
+        @type int
+        """
+        if index < 0:
+            # backward
+            for _ind in range(-index):
+                self.backward()
+        else:
+            # forward
+            for _ind in range(index):
+                self.forward()
+    
+    def isBackwardAvailable(self):
+        """
+        Public method to check, if stepping backward through the history is
+        available.
+        
+        @return flag indicating backward stepping is available
+        @rtype bool
+        """
+        return QTextBrowser.isBackwardAvailable(self)
+    
+    def isForwardAvailable(self):
+        """
+        Public method to check, if stepping forward through the history is
+        available.
+        
+        @return flag indicating forward stepping is available
+        @rtype bool
+        """
+        return QTextBrowser.isForwardAvailable(self)
+    
+    def scaleUp(self):
+        """
+        Public method to zoom in.
+        """
+        if self.__zoomCount < 10:
+            self.__zoomCount += 1
+            self.zoomIn()
+            self.zoomChanged.emit()
+    
+    def scaleDown(self):
+        """
+        Public method to zoom out.
+        """
+        if self.__zoomCount > -5:
+            self.__zoomCount -= 1
+            self.zoomOut()
+            self.zoomChanged.emit()
+    
+    def setScale(self, scale):
+        """
+        Public method to set the zoom level.
+        
+        @param scale zoom level to set
+        @type int
+        """
+        if -5 <= scale <= 10:
+            self.zoomOut(scale)
+            self.__zoomCount = scale
+            self.zoomChanged.emit()
+    
+    def resetScale(self):
+        """
+        Public method to reset the zoom level.
+        """
+        if self.__zoomCount != 0:
+            self.zoomOut(self.__zoomCount)
+            self.zoomChanged.emit()
+        self.__zoomCount = 0
+    
+    def scale(self):
+        """
+        Public method to get the zoom level.
+        
+        @return current zoom level
+        @rtype int
+        """
+        return self.__zoomCount
+    
+    def isScaleUpAvailable(self):
+        """
+        Public method to check, if the max. zoom level is reached.
+        
+        @return flag indicating scale up is available
+        @rtype bool
+        """
+        return self.__zoomCount < 10
+    
+    def isScaleDownAvailable(self):
+        """
+        Public method to check, if the min. zoom level is reached.
+        
+        @return flag indicating scale down is available
+        @rtype bool
+        """
+        return self.__zoomCount > -5
+    
+    def wheelEvent(self, evt):
+        """
+        Protected method to handle wheel event to zoom.
+        
+        @param evt reference to the event object
+        @type QWheelEvent
+        """
+        delta = evt.angleDelta().y()
+        if evt.modifiers() == Qt.KeyboardModifier.ControlModifier:
+            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)
+    
+    def keyPressEvent(self, evt):
+        """
+        Protected method to handle key press events.
+        
+        @param evt reference to the key event
+        @type QKeyEvent
+        """
+        key = evt.key()
+        isControlModifier = bool(
+            evt.modifiers() & Qt.KeyboardModifier.ControlModifier)
+        
+        if (
+            key == Qt.Key.Key_ZoomIn or
+            (key == Qt.Key.Key_Plus and isControlModifier)
+        ):
+            self.scaleUp()
+            evt.accept()
+        elif (
+            key == Qt.Key.Key_ZoomOut or
+            (key == Qt.Key.Key_Minus and isControlModifier)
+        ):
+            self.scaleDown()
+            evt.accept()
+        elif key == Qt.Key.Key_0 and isControlModifier:
+            self.resetScale()
+            evt.accept()
+        elif (
+            key == Qt.Key.Key_Backspace or
+            (key == Qt.Key.Key_Left and isControlModifier)
+        ):
+            self.backward()
+            evt.accept()
+        elif key == Qt.Key.Key_Right and isControlModifier:
+            self.forward()
+            evt.accept()
+        else:
+            super().keyPressEvent(evt)
+    
+    def event(self, evt):
+        """
+        Public method handling events.
+        
+        @param evt reference to the event
+        @type QEvent
+        @return flag indicating the event was handled
+        @rtype bool
+        """
+        if evt.type() == QEvent.Type.Gesture:
+            self.gestureEvent(evt)
+            return True
+        
+        return super().event(evt)
+    
+    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:
+                zoom = (self.getZoom() + 6) / 10.0
+                pinch.setTotalScaleFactor(zoom)
+            elif pinch.state() == Qt.GestureState.GestureUpdated:
+                zoom = int(pinch.totalScaleFactor() * 10) - 6
+                if zoom <= -5:
+                    zoom = -5
+                    pinch.setTotalScaleFactor(0.1)
+                elif zoom >= 10:
+                    zoom = 10
+                    pinch.setTotalScaleFactor(1.6)
+                self.setScale(zoom)
+            evt.accept()
+    
+    # TODO: add Ctrl+LMB action (open link in new page)
+    
+    #######################################################################
+    ## Context menu related methods below
+    #######################################################################
+    
+    @pyqtSlot(QPoint)
+    def __showContextMenu(self, pos):
+        """
+        Private slot to show the context menu.
+        
+        @param pos position to show the context menu at
+        @type QPoint
+        """
+        self.__menu.clear()
+        anchor = self.anchorAt(pos)
+        linkUrl = self.link().resolved(QUrl(anchor)) if anchor else QUrl()
+        selectedText = self.textCursor().selectedText()
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("back"),
+            self.tr("Backward"),
+            self.backward)
+        act.setEnabled(self.isBackwardAvailable())
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("forward"),
+            self.tr("Forward"),
+            self.forward)
+        act.setEnabled(self.isForwardAvailable())
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("reload"),
+            self.tr("Reload"),
+            self.reload)
+        
+        if not linkUrl.isEmpty() and linkUrl.scheme() != "javascript":
+            self.__createLinkContextMenu(self.__menu, linkUrl)
+        
+        self.__menu.addSeparator()
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy Page URL to Clipboard"))
+        act.setData(self.link())
+        act.triggered.connect(
+            functools.partial(self.__copyLink, act))
+        
+        self.__menu.addSeparator()
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("zoomIn"),
+            self.tr("Zoom in"),
+            self.scaleUp)
+        act.setEnabled(self.isScaleUpAvailable())
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("zoomOut"),
+            self.tr("Zoom out"),
+            self.scaleDown)
+        act.setEnabled(self.isScaleDownAvailable())
+        
+        self.__menu.addAction(
+            UI.PixmapCache.getIcon("zoomReset"),
+            self.tr("Zoom reset"),
+            self.resetScale)
+        
+        self.__menu.addSeparator()
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy"),
+            self.copy)
+        act.setEnabled(bool(selectedText))
+        
+        self.__menu.addAction(
+            UI.PixmapCache.getIcon("editSelectAll"),
+            self.tr("Select All"),
+            self.selectAll)
+        
+        self.__menu.addSeparator()
+        
+        self.__menu.addAction(
+            UI.PixmapCache.getIcon("tabClose"),
+            self.tr('Close'),
+            self.__closePage)
+        
+        act = self.__menu.addAction(
+            UI.PixmapCache.getIcon("tabCloseOther"),
+            self.tr("Close Others"),
+            self.__closeOtherPages)
+        act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1)
+        
+        self.__menu.popup(self.mapToGlobal(pos))
+    
+    def __createLinkContextMenu(self, menu, linkUrl):
+        """
+        Private method to populate the context menu for URLs.
+        
+        @param menu reference to the menu to be populated
+        @type QMenu
+        @param linkUrl URL to create the menu part for
+        @type QUrl
+        """
+        if not menu.isEmpty():
+            menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("openNewTab"),
+            self.tr("Open Link in New Page"))
+        act.setData(linkUrl)
+        act.triggered.connect(
+            functools.partial(self.__openLinkInNewPage, act))
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("newWindow"),
+            self.tr("Open Link in Background Page"))
+        act.setData(linkUrl)
+        act.triggered.connect(
+            functools.partial(self.__openLinkInBackgroundPage, act))
+        
+        menu.addSeparator()
+        
+        act = menu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy URL to Clipboard"))
+        act.setData(linkUrl)
+        act.triggered.connect(
+            functools.partial(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
+        """
+        url = act.data()
+        if url.isEmpty():
+            return
+        
+        self.__helpViewerWidget.openUrlNewPage(url)
+    
+    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
+        """
+        url = act.data()
+        if url.isEmpty():
+            return
+        
+        self.__helpViewerWidget.openUrlNewBackgroundPage(url)
+    
+    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
+        """
+        data = act.data()
+        if isinstance(data, QUrl) and data.isEmpty():
+            return
+        
+        if isinstance(data, QUrl):
+            data = data.toString()
+        
+        # copy the URL to both clipboard areas
+        QGuiApplication.clipboard().setText(data, QClipboard.Mode.Clipboard)
+        QGuiApplication.clipboard().setText(data, QClipboard.Mode.Selection)
+    
+    def __closePage(self):
+        """
+        Private method called by the context menu to close the current page.
+        """
+        self.__helpViewerWidget.closeCurrentPage()
+    
+    def __closeOtherPages(self):
+        """
+        Private method called by the context menu to close all other pages.
+        """
+        self.__helpViewerWidget.closeOtherPages()

eric ide

mercurial