src/eric7/WebBrowser/WebBrowserView.py

Mon, 24 Feb 2025 15:43:49 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 24 Feb 2025 15:43:49 +0100
branch
eric7
changeset 11148
15e30f0c76a8
parent 11090
f5f5f5803935
permissions
-rw-r--r--

Adjusted the code to the modified issue codes.

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

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


"""
Module implementing the web browser using QWebEngineView.
"""

import contextlib
import functools
import os
import pathlib

from PyQt6.QtCore import (
    QByteArray,
    QDataStream,
    QDateTime,
    QEvent,
    QEventLoop,
    QIODevice,
    QMarginsF,
    QPoint,
    QPointF,
    QStandardPaths,
    Qt,
    QTimer,
    QUrl,
    pyqtSignal,
    pyqtSlot,
)
from PyQt6.QtGui import (
    QClipboard,
    QCursor,
    QDesktopServices,
    QIcon,
    QPageLayout,
    QPixmap,
)
from PyQt6.QtPrintSupport import (
    QAbstractPrintDialog,
    QPrintDialog,
    QPrinter,
    QPrintPreviewDialog,
)
from PyQt6.QtWebEngineCore import (
    QWebEngineDownloadRequest,
    QWebEnginePage,
    QWebEngineWebAuthUxRequest,
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QApplication, QDialog, QMenu, QStyle

from eric7 import EricUtilities, Preferences
from eric7.__version__ import VersionOnly
from eric7.EricGui import EricPixmapCache
from eric7.EricWidgets import EricFileDialog, EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp
from eric7.SystemUtilities import FileSystemUtilities, OSUtilities
from eric7.UI.Info import Homepage
from eric7.WebBrowser.WebBrowserWindow import WebBrowserWindow
from eric7.WebBrowser.ZoomManager import ZoomManager

from . import WebInspector
from .Tools import Scripts, WebBrowserTools
from .Tools.WebBrowserTools import getHtmlPage, pixmapToDataUrl
from .Tools.WebIconLoader import WebIconLoader
from .WebBrowserPage import WebBrowserPage


def isCupsAvailable():
    """
    Static method to test the availability of CUPS.

    @return flag indicating the availability of CUPS
    @rtype bool
    """
    if OSUtilities.isMacPlatform():
        # OS X/MacOS always have CUPS
        return True
    elif OSUtilities.isLinuxPlatform() or OSUtilities.isFreeBsdPlatform():
        testPrinter = QPrinter()
        return testPrinter.supportsMultipleCopies()
    else:
        return False


class WebBrowserView(QWebEngineView):
    """
    Class implementing the web browser view widget.

    @signal sourceChanged(QUrl) emitted after the current URL has changed
    @signal forwardAvailable(bool) emitted after the current URL has changed
    @signal backwardAvailable(bool) emitted after the current URL has changed
    @signal highlighted(str) emitted, when the mouse hovers over a link
    @signal search(QUrl) emitted, when a search is requested
    @signal zoomValueChanged(int) emitted to signal a change of the zoom value
    @signal faviconChanged() emitted to signal a changed web site icon
    @signal safeBrowsingAbort() emitted to indicate an abort due to a safe
        browsing event
    @signal safeBrowsingBad(threatType, threatMessages) emitted to indicate a
        malicious web site as determined by safe browsing
    @signal showMessage(str) emitted to show a message in the main window
        status bar
    """

    sourceChanged = pyqtSignal(QUrl)
    forwardAvailable = pyqtSignal(bool)
    backwardAvailable = pyqtSignal(bool)
    highlighted = pyqtSignal(str)
    search = pyqtSignal(QUrl)
    zoomValueChanged = pyqtSignal(int)
    faviconChanged = pyqtSignal()
    safeBrowsingAbort = pyqtSignal()
    safeBrowsingBad = pyqtSignal(str, str)
    showMessage = pyqtSignal(str)

    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, mainWindow, parent=None, name=""):
        """
        Constructor

        @param mainWindow reference to the main window
        @type WebBrowserWindow
        @param parent parent widget of this window
        @type QWidget
        @param name name of this window
        @type str
        """
        super().__init__(parent)
        self.setObjectName(name)

        self.__rwhvqt = None
        self.installEventFilter(self)

        self.__speedDial = WebBrowserWindow.speedDial()

        self.__page = None
        self.__createNewPage()

        self.__mw = mainWindow
        self.__tabWidget = parent
        self.__isLoading = False
        self.__progress = 0
        self.__siteIconLoader = None
        self.__siteIcon = QIcon()
        self.__menu = QMenu(self)
        self.__clickedPos = QPoint()
        self.__firstLoad = False
        self.__preview = QPixmap()
        self.__currentPrinter = None
        self.__printPreviewLoop = None
        self.__webAuthDialog = None

        self.__currentZoom = 100
        self.__zoomLevels = WebBrowserView.ZoomLevels[:]

        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)

        self.iconUrlChanged.connect(self.__iconUrlChanged)
        self.urlChanged.connect(self.__urlChanged)
        self.page().linkHovered.connect(self.__linkHovered)

        self.loadStarted.connect(self.__loadStarted)
        self.loadProgress.connect(self.__loadProgress)
        self.loadFinished.connect(self.__loadFinished)
        self.renderProcessTerminated.connect(self.__renderProcessTerminated)

        self.printRequested.connect(self.printPage)
        self.printFinished.connect(self.__printPageFinished)
        self.pdfPrintingFinished.connect(self.__printPageToPdfFinished)

        self.__mw.openSearchManager().currentEngineChanged.connect(
            self.__currentEngineChanged
        )

        self.setAcceptDrops(True)

        self.__rss = []

        self.__clickedFrame = None

        self.__mw.personalInformationManager().connectPage(self.page())

        self.__inspector = None
        WebInspector.registerView(self)

        self.__restoreData = None

        if self.parentWidget() is not None:
            self.parentWidget().installEventFilter(self)

        self.grabGesture(Qt.GestureType.PinchGesture)

    def __createNewPage(self):
        """
        Private method to create a new page object.
        """
        self.__page = WebBrowserPage(self, self)
        self.setPage(self.__page)

        self.__page.safeBrowsingAbort.connect(self.safeBrowsingAbort)
        self.__page.safeBrowsingBad.connect(self.safeBrowsingBad)
        with contextlib.suppress(AttributeError):
            # deprecated with Qt 6.5+
            self.__page.quotaRequested.connect(self.__quotaRequested)
        with contextlib.suppress(AttributeError):
            # Qt 6.4+
            self.__page.fileSystemAccessRequested.connect(
                self.__fileSystemAccessRequested
            )
        with contextlib.suppress(AttributeError):
            # Qt 6.7+
            self.__page.webAuthUxRequested.connect(self.__webAuthUxRequested)
        # The registerProtocolHandlerRequested signal is handled in
        # WebBrowserPage.
        self.__page.selectClientCertificate.connect(self.__selectClientCertificate)
        self.__page.findTextFinished.connect(self.__findTextFinished)

    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_M-801__

    def __currentEngineChanged(self):
        """
        Private slot to track a change of the current search engine.
        """
        if self.url().toString() == "eric:home":
            self.reload()

    def mainWindow(self):
        """
        Public method to get a reference to the main window.

        @return reference to the main window
        @rtype WebBrowserWindow
        """
        return self.__mw

    def tabWidget(self):
        """
        Public method to get a reference to the tab widget containing this
        view.

        @return reference to the tab widget
        @rtype WebBrowserTabWidget
        """
        return self.__tabWidget

    def load(self, url):
        """
        Public method to load a web site.

        @param url URL to be loaded
        @type QUrl
        """
        if self.__page is not None and not self.__page.acceptNavigationRequest(
            url, QWebEnginePage.NavigationType.NavigationTypeTyped, True
        ):
            return

        super().load(url)

        if not self.__firstLoad:
            self.__firstLoad = True
            WebInspector.pushView(self)

    def setSource(self, name, newTab=False):
        """
        Public method used to set the source to be displayed.

        @param name filename to be shown
        @type QUrl
        @param newTab flag indicating to open the URL in a new tab
        @type bool
        """
        if name is None or not name.isValid():
            return

        if newTab:
            # open in a new tab
            self.__mw.newTab(name)
            return

        if not name.scheme():
            if not os.path.exists(name.toString()):
                name.setScheme(Preferences.getWebBrowser("DefaultScheme"))
            else:
                if OSUtilities.isWindowsPlatform():
                    name.setUrl(
                        "file:///"
                        + FileSystemUtilities.fromNativeSeparators(name.toString())
                    )
                else:
                    name.setUrl("file://" + name.toString())

        if len(name.scheme()) == 1 or name.scheme() == "file":
            # name is a local file
            if name.scheme() and len(name.scheme()) == 1:
                # it is a local path on win os
                name = QUrl.fromLocalFile(name.toString())

            if not pathlib.Path(name.toLocalFile()).exists():
                EricMessageBox.critical(
                    self,
                    self.tr("eric Web Browser"),
                    self.tr("""<p>The file <b>{0}</b> does not exist.</p>""").format(
                        name.toLocalFile()
                    ),
                )
                return

            if name.toLocalFile().lower().endswith((".pdf", ".chm")):
                started = QDesktopServices.openUrl(name)
                if not started:
                    EricMessageBox.critical(
                        self,
                        self.tr("eric Web Browser"),
                        self.tr(
                            """<p>Could not start a viewer"""
                            """ for file <b>{0}</b>.</p>"""
                        ).format(name.path()),
                    )
                return
        elif name.scheme() in ["mailto"]:
            started = QDesktopServices.openUrl(name)
            if not started:
                EricMessageBox.critical(
                    self,
                    self.tr("eric Web Browser"),
                    self.tr(
                        """<p>Could not start an application"""
                        """ for URL <b>{0}</b>.</p>"""
                    ).format(name.toString()),
                )
            return
        else:
            if name.toString().lower().endswith((".pdf", ".chm")):
                started = QDesktopServices.openUrl(name)
                if not started:
                    EricMessageBox.critical(
                        self,
                        self.tr("eric Web Browser"),
                        self.tr(
                            """<p>Could not start a viewer"""
                            """ for file <b>{0}</b>.</p>"""
                        ).format(name.path()),
                    )
                return

        self.load(name)

    def source(self):
        """
        Public method to return the URL of the loaded page.

        @return URL loaded in the help browser
        @rtype QUrl
        """
        return self.url()

    def documentTitle(self):
        """
        Public method to return the title of the loaded page.

        @return title
        @rtype str
        """
        return self.title()

    def backward(self):
        """
        Public slot to move backwards in history.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Back)
        self.__urlChanged(self.history().currentItem().url())

    def forward(self):
        """
        Public slot to move forward in history.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Forward)
        self.__urlChanged(self.history().currentItem().url())

    def home(self):
        """
        Public slot to move to the first page loaded.
        """
        homeUrl = QUrl(Preferences.getWebBrowser("HomePage"))
        self.setSource(homeUrl)
        self.__urlChanged(self.history().currentItem().url())

    def reload(self):
        """
        Public slot to reload the current page.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Reload)

    def reloadBypassingCache(self):
        """
        Public slot to reload the current page bypassing the cache.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.ReloadAndBypassCache)

    def copy(self):
        """
        Public slot to copy the selected text.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Copy)

    def cut(self):
        """
        Public slot to cut the selected text.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Cut)

    def paste(self):
        """
        Public slot to paste text from the clipboard.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Paste)

    def undo(self):
        """
        Public slot to undo the last edit action.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Undo)

    def redo(self):
        """
        Public slot to redo the last edit action.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Redo)

    def selectAll(self):
        """
        Public slot to select all text.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.SelectAll)

    def unselect(self):
        """
        Public slot to clear the current selection.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.Unselect)

    def isForwardAvailable(self):
        """
        Public method to determine, if a forward move in history is possible.

        @return flag indicating move forward is possible
        @rtype bool
        """
        return self.history().canGoForward()

    def isBackwardAvailable(self):
        """
        Public method to determine, if a backwards move in history is possible.

        @return flag indicating move backwards is possible
        @rtype bool
        """
        return self.history().canGoBack()

    def __levelForZoom(self, zoom):
        """
        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(zoom)
        except ValueError:
            for index in range(len(self.__zoomLevels)):
                if zoom <= self.__zoomLevels[index]:
                    break
        return index

    def setZoomValue(self, value, saveValue=True):
        """
        Public method to set the zoom value.

        @param value zoom value
        @type int
        @param saveValue flag indicating to save the zoom value with the
            zoom manager
        @type bool
        """
        if value != self.__currentZoom:
            self.setZoomFactor(value / 100.0)
            self.__currentZoom = value
            if saveValue and not self.__mw.isPrivate():
                ZoomManager.instance().setZoomValue(self.url(), value)
            self.zoomValueChanged.emit(value)

    def zoomValue(self):
        """
        Public method to get the current zoom value.

        @return zoom value
        @rtype int
        """
        val = self.zoomFactor() * 100
        return int(val)

    def zoomIn(self):
        """
        Public slot to zoom into the page.
        """
        index = self.__levelForZoom(self.__currentZoom)
        if index < len(self.__zoomLevels) - 1:
            self.setZoomValue(self.__zoomLevels[index + 1])

    def zoomOut(self):
        """
        Public slot to zoom out of the page.
        """
        index = self.__levelForZoom(self.__currentZoom)
        if index > 0:
            self.setZoomValue(self.__zoomLevels[index - 1])

    def zoomReset(self):
        """
        Public method to reset the zoom factor.
        """
        index = self.__levelForZoom(WebBrowserView.ZoomLevelDefault)
        self.setZoomValue(self.__zoomLevels[index])

    def mapToViewport(self, pos):
        """
        Public method to map a position to the viewport.

        @param pos position to be mapped
        @type QPoint
        @return viewport position
        @rtype QPoint
        """
        return self.page().mapToViewport(pos)

    def hasSelection(self):
        """
        Public method to determine, if there is some text selected.

        @return flag indicating text has been selected
        @rtype bool
        """
        return self.selectedText() != ""

    def findNextPrev(self, txt, case, backwards, callback):
        """
        Public slot to find the next occurrence of a text.

        @param txt text to search for
        @type str
        @param case flag indicating a case sensitive search
        @type bool
        @param backwards flag indicating a backwards search
        @type bool
        @param callback reference to a function with a bool parameter
        @type function(bool) or None
        """
        findFlags = QWebEnginePage.FindFlag(0)
        if case:
            findFlags |= QWebEnginePage.FindFlag.FindCaseSensitively
        if backwards:
            findFlags |= QWebEnginePage.FindFlag.FindBackward

        if callback is None:
            self.findText(txt, findFlags)
        else:
            self.findText(txt, findFlags, callback)

    def __findTextFinished(self, result):
        """
        Private slot handling the findTextFinished signal of the web page.

        @param result reference to the QWebEngineFindTextResult object of the
            last search
        @type QWebEngineFindTextResult
        """
        self.showMessage.emit(
            self.tr("Match {0} of {1}").format(
                result.activeMatch(), result.numberOfMatches()
            )
        )

    @pyqtSlot(QPoint)
    def __showContextMenu(self, pos):
        """
        Private slot to show a context menu.

        @param pos position for the context menu
        @type QPoint
        """
        self.__menu.clear()

        hitTest = self.page().hitTestContent(pos)

        self.__createContextMenu(self.__menu, hitTest)

        if not hitTest.isContentEditable() and not hitTest.isContentSelected():
            self.__menu.addSeparator()
            self.__menu.addMenu(self.__mw.adBlockIcon().menu())

        self.__menu.addSeparator()
        self.__menu.addAction(
            EricPixmapCache.getIcon("webInspector"),
            self.tr("Inspect Element..."),
            self.__webInspector,
        )

        if not self.__menu.isEmpty():
            self.__menu.popup(self.mapToGlobal(pos))

    def __createContextMenu(self, menu, hitTest):
        """
        Private method to populate the context menu.

        @param menu reference to the menu to be populated
        @type QMenu
        @param hitTest reference to the hit test object
        @type WebHitTestResult
        """
        spellCheckActionCount = 0
        contextMenuData = self.lastContextMenuRequest()
        hitTest.updateWithContextMenuData(contextMenuData)

        if bool(contextMenuData.misspelledWord()):
            boldFont = menu.font()
            boldFont.setBold(True)

            for suggestion in contextMenuData.spellCheckerSuggestions():
                act = menu.addAction(suggestion)
                act.setFont(boldFont)
                act.triggered.connect(
                    functools.partial(self.__replaceMisspelledWord, act)
                )

            if not bool(menu.actions()):
                menu.addAction(self.tr("No suggestions")).setEnabled(False)

            menu.addSeparator()
            spellCheckActionCount = len(menu.actions())

        if (
            not hitTest.linkUrl().isEmpty()
            and hitTest.linkUrl().scheme() != "javascript"
        ):
            self.__createLinkContextMenu(menu, hitTest)

        if not hitTest.imageUrl().isEmpty():
            self.__createImageContextMenu(menu, hitTest)

        if not hitTest.mediaUrl().isEmpty():
            self.__createMediaContextMenu(menu, hitTest)

        if hitTest.isContentEditable():
            # check, if only spell checker actions were added
            if len(menu.actions()) == spellCheckActionCount:
                menu.addAction(self.__mw.undoAct)
                menu.addAction(self.__mw.redoAct)
                menu.addSeparator()
                menu.addAction(self.__mw.cutAct)
                menu.addAction(self.__mw.copyAct)
                menu.addAction(self.__mw.pasteAct)
                menu.addSeparator()
                self.__mw.personalInformationManager().createSubMenu(
                    menu, self, hitTest
                )

            if hitTest.tagName() == "input":
                menu.addSeparator()
                act = menu.addAction("")
                act.setVisible(False)
                self.__checkForForm(act, hitTest.pos())

        if self.selectedText():
            self.__createSelectedTextContextMenu(menu, hitTest)

        if self.__menu.isEmpty():
            self.__createPageContextMenu(menu)

    def __createLinkContextMenu(self, menu, hitTest):
        """
        Private method to populate the context menu for URLs.

        @param menu reference to the menu to be populated
        @type QMenu
        @param hitTest reference to the hit test object
        @type WebHitTestResult
        """
        if not menu.isEmpty():
            menu.addSeparator()

        act = menu.addAction(
            EricPixmapCache.getIcon("openNewTab"),
            self.tr("Open Link in New Tab\tCtrl+LMB"),
        )
        act.setData(hitTest.linkUrl())
        act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))
        act = menu.addAction(
            EricPixmapCache.getIcon("newWindow"), self.tr("Open Link in New Window")
        )
        act.setData(hitTest.linkUrl())
        act.triggered.connect(functools.partial(self.__openLinkInNewWindow, act))
        act = menu.addAction(
            EricPixmapCache.getIcon("privateMode"),
            self.tr("Open Link in New Private Window"),
        )
        act.setData(hitTest.linkUrl())
        act.triggered.connect(functools.partial(self.__openLinkInNewPrivateWindow, act))
        menu.addSeparator()
        menu.addAction(
            EricPixmapCache.getIcon("download"),
            self.tr("Save Lin&k"),
            self.__downloadLink,
        )
        act = menu.addAction(
            EricPixmapCache.getIcon("bookmark22"), self.tr("Bookmark this Link")
        )
        act.setData(hitTest.linkUrl())
        act.triggered.connect(functools.partial(self.__bookmarkLink, act))
        menu.addSeparator()
        act = menu.addAction(
            EricPixmapCache.getIcon("editCopy"), self.tr("Copy URL to Clipboard")
        )
        act.setData(hitTest.linkUrl())
        act.triggered.connect(functools.partial(self.__copyLink, act))
        act = menu.addAction(EricPixmapCache.getIcon("mailSend"), self.tr("Send URL"))
        act.setData(hitTest.linkUrl())
        act.triggered.connect(functools.partial(self.__sendLink, act))
        if (
            Preferences.getWebBrowser("VirusTotalEnabled")
            and Preferences.getWebBrowser("VirusTotalServiceKey") != ""
        ):
            act = menu.addAction(
                EricPixmapCache.getIcon("virustotal"),
                self.tr("Scan Link with VirusTotal"),
            )
            act.setData(hitTest.linkUrl())
            act.triggered.connect(functools.partial(self.__virusTotal, act))

    def __createImageContextMenu(self, menu, hitTest):
        """
        Private method to populate the context menu for images.

        @param menu reference to the menu to be populated
        @type QMenu
        @param hitTest reference to the hit test object
        @type WebHitTestResult
        """
        if not menu.isEmpty():
            menu.addSeparator()

        act = menu.addAction(
            EricPixmapCache.getIcon("openNewTab"), self.tr("Open Image in New Tab")
        )
        act.setData(hitTest.imageUrl())
        act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))
        menu.addSeparator()
        menu.addAction(
            EricPixmapCache.getIcon("download"),
            self.tr("Save Image"),
            self.__downloadImage,
        )
        menu.addAction(self.tr("Copy Image to Clipboard"), self.__copyImage)
        act = menu.addAction(
            EricPixmapCache.getIcon("editCopy"), self.tr("Copy Image URL to Clipboard")
        )
        act.setData(hitTest.imageUrl())
        act.triggered.connect(functools.partial(self.__copyLink, act))
        act = menu.addAction(
            EricPixmapCache.getIcon("mailSend"), self.tr("Send Image URL")
        )
        act.setData(hitTest.imageUrl())
        act.triggered.connect(functools.partial(self.__sendLink, act))

        if hitTest.imageUrl().scheme() in ["http", "https"]:
            menu.addSeparator()
            engine = WebBrowserWindow.imageSearchEngine()
            searchEngineName = engine.searchEngine()
            act = menu.addAction(
                EricPixmapCache.getIcon("{0}".format(searchEngineName.lower())),
                self.tr("Search image in {0}").format(searchEngineName),
            )
            act.setData(engine.getSearchQuery(hitTest.imageUrl()))
            act.triggered.connect(functools.partial(self.__searchImage, act))
            self.__imageSearchMenu = menu.addMenu(self.tr("Search image with..."))
            for searchEngineName in engine.searchEngineNames():
                act = self.__imageSearchMenu.addAction(
                    EricPixmapCache.getIcon("{0}".format(searchEngineName.lower())),
                    self.tr("Search image in {0}").format(searchEngineName),
                )
                act.setData(engine.getSearchQuery(hitTest.imageUrl(), searchEngineName))
                act.triggered.connect(functools.partial(self.__searchImage, act))

        menu.addSeparator()
        act = menu.addAction(
            EricPixmapCache.getIcon("adBlockPlus"), self.tr("Block Image")
        )
        act.setData(hitTest.imageUrl().toString())
        act.triggered.connect(functools.partial(self.__blockImage, act))
        if (
            Preferences.getWebBrowser("VirusTotalEnabled")
            and Preferences.getWebBrowser("VirusTotalServiceKey") != ""
        ):
            act = menu.addAction(
                EricPixmapCache.getIcon("virustotal"),
                self.tr("Scan Image with VirusTotal"),
            )
            act.setData(hitTest.imageUrl())
            act.triggered.connect(functools.partial(self.__virusTotal, act))

    def __createMediaContextMenu(self, menu, hitTest):
        """
        Private method to populate the context menu for media elements.

        @param menu reference to the menu to be populated
        @type QMenu
        @param hitTest reference to the hit test object
        @type WebHitTestResult
        """
        if not menu.isEmpty():
            menu.addSeparator()

        if hitTest.mediaPaused():
            menu.addAction(
                EricPixmapCache.getIcon("mediaPlaybackStart"),
                self.tr("Play"),
                self.__pauseMedia,
            )
        else:
            menu.addAction(
                EricPixmapCache.getIcon("mediaPlaybackPause"),
                self.tr("Pause"),
                self.__pauseMedia,
            )
        if hitTest.mediaMuted():
            menu.addAction(
                EricPixmapCache.getIcon("audioVolumeHigh"),
                self.tr("Unmute"),
                self.__muteMedia,
            )
        else:
            menu.addAction(
                EricPixmapCache.getIcon("audioVolumeMuted"),
                self.tr("Mute"),
                self.__muteMedia,
            )
        menu.addSeparator()
        act = menu.addAction(
            EricPixmapCache.getIcon("editCopy"), self.tr("Copy Media URL to Clipboard")
        )
        act.setData(hitTest.mediaUrl())
        act.triggered.connect(functools.partial(self.__copyLink, act))
        act = menu.addAction(
            EricPixmapCache.getIcon("mailSend"), self.tr("Send Media URL")
        )
        act.setData(hitTest.mediaUrl())
        act.triggered.connect(functools.partial(self.__sendLink, act))
        menu.addAction(
            EricPixmapCache.getIcon("download"),
            self.tr("Save Media"),
            self.__downloadMedia,
        )

    def __createSelectedTextContextMenu(self, menu, _hitTest):
        """
        Private method to populate the context menu for selected text.

        @param menu reference to the menu to be populated
        @type QMenu
        @param _hitTest reference to the hit test object (unused)
        @type WebHitTestResult
        """
        from .OpenSearch.OpenSearchEngineAction import OpenSearchEngineAction
        from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog

        if not menu.isEmpty():
            menu.addSeparator()

        menu.addAction(self.__mw.copyAct)
        menu.addSeparator()
        act = menu.addAction(EricPixmapCache.getIcon("mailSend"), self.tr("Send Text"))
        act.setData(self.selectedText())
        act.triggered.connect(functools.partial(self.__sendLink, act))

        engineName = self.__mw.openSearchManager().currentEngineName()
        if engineName:
            menu.addAction(
                self.tr("Search with '{0}'").format(engineName),
                self.__searchDefaultRequested,
            )

        self.__searchMenu = menu.addMenu(self.tr("Search with..."))
        engineNames = self.__mw.openSearchManager().allEnginesNames()
        for engineName in engineNames:
            engine = self.__mw.openSearchManager().engine(engineName)
            act = OpenSearchEngineAction(engine, self.__searchMenu)
            act.setData(engineName)
            self.__searchMenu.addAction(act)
        self.__searchMenu.triggered.connect(self.__searchRequested)

        menu.addSeparator()

        languages = EricUtilities.toList(
            Preferences.getSettings().value(
                "WebBrowser/AcceptLanguages",
                WebBrowserLanguagesDialog.defaultAcceptLanguages(),
            )
        )
        if languages:
            language = languages[0]
            langCode = language.split("[")[1][:2]
            googleTranslatorUrl = QUrl(
                "http://translate.google.com/#auto/{0}/{1}".format(
                    langCode, self.selectedText()
                )
            )
            act = menu.addAction(
                EricPixmapCache.getIcon("translate"), self.tr("Google Translate")
            )
            act.setData(googleTranslatorUrl)
            act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))
            wiktionaryUrl = QUrl(
                "http://{0}.wiktionary.org/wiki/Special:Search?search={1}".format(
                    langCode, self.selectedText()
                )
            )
            act = menu.addAction(
                EricPixmapCache.getIcon("wikipedia"), self.tr("Dictionary")
            )
            act.setData(wiktionaryUrl)
            act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))
            menu.addSeparator()

        guessedUrl = QUrl.fromUserInput(self.selectedText().strip())
        if self.__isUrlValid(guessedUrl):
            act = menu.addAction(self.tr("Go to web address"))
            act.setData(guessedUrl)
            act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))

    def __createPageContextMenu(self, menu):
        """
        Private method to populate the basic context menu.

        @param menu reference to the menu to be populated
        @type QMenu
        """
        from .UserAgent.UserAgentMenu import UserAgentMenu
        from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog

        menu.addAction(self.__mw.newTabAct)
        menu.addAction(self.__mw.newAct)
        menu.addSeparator()
        if self.__mw.saveAsAct is not None:
            menu.addAction(self.__mw.saveAsAct)
        menu.addAction(self.__mw.saveVisiblePageScreenAct)
        menu.addSeparator()

        if self.url().toString() == "eric:speeddial":
            # special menu for the spedd dial page
            menu.addAction(self.__mw.backAct)
            menu.addAction(self.__mw.forwardAct)
            menu.addSeparator()
            menu.addAction(
                EricPixmapCache.getIcon("plus"),
                self.tr("Add New Page"),
                self.__addSpeedDial,
            )
            menu.addAction(
                EricPixmapCache.getIcon("preferences-general"),
                self.tr("Configure Speed Dial"),
                self.__configureSpeedDial,
            )
            menu.addSeparator()
            menu.addAction(
                EricPixmapCache.getIcon("reload"),
                self.tr("Reload All Dials"),
                self.__reloadAllSpeedDials,
            )
            menu.addSeparator()
            menu.addAction(self.tr("Reset to Default Dials"), self.__resetSpeedDials)
            return

        menu.addAction(
            EricPixmapCache.getIcon("bookmark22"),
            self.tr("Bookmark this Page"),
            self.addBookmark,
        )
        act = menu.addAction(
            EricPixmapCache.getIcon("editCopy"), self.tr("Copy Page URL to Clipboard")
        )
        act.setData(self.url())
        act.triggered.connect(functools.partial(self.__copyLink, act))
        act = menu.addAction(
            EricPixmapCache.getIcon("mailSend"), self.tr("Send Page URL")
        )
        act.setData(self.url())
        act.triggered.connect(functools.partial(self.__sendLink, act))
        menu.addSeparator()

        self.__userAgentMenu = UserAgentMenu(self.tr("User Agent"), url=self.url())
        menu.addMenu(self.__userAgentMenu)
        menu.addSeparator()
        menu.addAction(self.__mw.backAct)
        menu.addAction(self.__mw.forwardAct)
        menu.addAction(self.__mw.homeAct)
        menu.addAction(self.__mw.reloadAct)
        menu.addAction(self.__mw.stopAct)
        menu.addSeparator()
        menu.addAction(self.__mw.zoomInAct)
        menu.addAction(self.__mw.zoomResetAct)
        menu.addAction(self.__mw.zoomOutAct)
        menu.addSeparator()
        menu.addAction(self.__mw.selectAllAct)
        menu.addSeparator()
        menu.addAction(self.__mw.findAct)
        menu.addSeparator()
        menu.addAction(self.__mw.pageSourceAct)
        menu.addSeparator()
        menu.addAction(self.__mw.siteInfoAct)
        if self.url().scheme() in ["http", "https"]:
            menu.addSeparator()

            w3url = QUrl.fromEncoded(
                b"http://validator.w3.org/check?uri="
                + QUrl.toPercentEncoding(bytes(self.url().toEncoded()).decode())
            )
            act = menu.addAction(
                EricPixmapCache.getIcon("w3"), self.tr("Validate Page")
            )
            act.setData(w3url)
            act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))

            languages = EricUtilities.toList(
                Preferences.getSettings().value(
                    "WebBrowser/AcceptLanguages",
                    WebBrowserLanguagesDialog.defaultAcceptLanguages(),
                )
            )
            if languages:
                language = languages[0]
                langCode = language.split("[")[1][:2]
                googleTranslatorUrl = QUrl.fromEncoded(
                    b"http://translate.google.com/translate?sl=auto&tl="
                    + langCode.encode()
                    + b"&u="
                    + QUrl.toPercentEncoding(bytes(self.url().toEncoded()).decode())
                )
                act = menu.addAction(
                    EricPixmapCache.getIcon("translate"), self.tr("Google Translate")
                )
                act.setData(googleTranslatorUrl)
                act.triggered.connect(functools.partial(self.__openLinkInNewTab, act))

    def __checkForForm(self, act, pos):
        """
        Private method to check the given position for an open search form.

        @param act reference to the action to be populated upon success
        @type QAction
        @param pos position to be tested
        @type QPoint
        """
        from .Tools import Scripts

        self.__clickedPos = self.mapToViewport(pos)

        script = Scripts.getFormData(self.__clickedPos)
        self.page().runJavaScript(
            script,
            WebBrowserPage.SafeJsWorld,
            lambda res: self.__checkForFormCallback(res, act),
        )

    def __checkForFormCallback(self, res, act):
        """
        Private method handling the __checkForForm result.

        @param res result dictionary generated by JavaScript
        @type dict
        @param act reference to the action to be populated upon success
        @type QAction
        """
        if act is None or not bool(res):
            return

        url = QUrl(res["action"])
        method = res["method"]

        if not url.isEmpty() and method in ["get", "post"]:
            act.setVisible(True)
            act.setText(self.tr("Add to web search toolbar"))
            act.triggered.connect(self.__addSearchEngine)

    def __isUrlValid(self, url):
        """
        Private method to check a URL for validity.

        @param url URL to be checked
        @type QUrl
        @return flag indicating a valid URL
        @rtype bool
        """
        return (
            url.isValid()
            and bool(url.host())
            and bool(url.scheme())
            and "." in url.host()
        )

    def __replaceMisspelledWord(self, act):
        """
        Private slot to replace a misspelled word under the context menu.

        @param act reference to the action that triggered
        @type QAction
        """
        suggestion = act.text()
        self.page().replaceMisspelledWord(suggestion)

    def __openLinkInNewTab(self, act):
        """
        Private method called by the context menu to open a link in a new
        tab.

        @param act reference to the action that triggered
        @type QAction
        """
        url = act.data()
        if url.isEmpty():
            return

        self.setSource(url, newTab=True)

    def __openLinkInNewWindow(self, act):
        """
        Private slot called by the context menu to open a link in a new
        window.

        @param act reference to the action that triggered
        @type QAction
        """
        url = act.data()
        if url.isEmpty():
            return

        self.__mw.newWindow(url)

    def __openLinkInNewPrivateWindow(self, act):
        """
        Private slot called by the context menu to open a link in a new
        private window.

        @param act reference to the action that triggered
        @type QAction
        """
        url = act.data()
        if url.isEmpty():
            return

        self.__mw.newPrivateWindow(url)

    def __bookmarkLink(self, act):
        """
        Private slot to bookmark a link via the context menu.

        @param act reference to the action that triggered
        @type QAction
        """
        from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog

        url = act.data()
        if url.isEmpty():
            return

        dlg = AddBookmarkDialog(parent=self)
        dlg.setUrl(bytes(url.toEncoded()).decode())
        dlg.exec()

    def __sendLink(self, act):
        """
        Private slot to send a link via email.

        @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()
        QDesktopServices.openUrl(QUrl("mailto:?body=" + data))

    def __copyLink(self, act):
        """
        Private slot 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
        QApplication.clipboard().setText(data, QClipboard.Mode.Clipboard)
        QApplication.clipboard().setText(data, QClipboard.Mode.Selection)

    def __downloadLink(self):
        """
        Private slot to download a link and save it to disk.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.DownloadLinkToDisk)

    def __downloadImage(self):
        """
        Private slot to download an image and save it to disk.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.DownloadImageToDisk)

    def __copyImage(self):
        """
        Private slot to copy an image to the clipboard.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.CopyImageToClipboard)

    def __blockImage(self, act):
        """
        Private slot to add a block rule for an image URL.

        @param act reference to the action that triggered
        @type QAction
        """
        url = act.data()
        dlg = WebBrowserWindow.adBlockManager().showDialog()
        dlg.addCustomRule(url)

    def __searchImage(self, act):
        """
        Private slot to search for an image URL.

        @param act reference to the action that triggered
        @type QAction
        """
        url = act.data()
        self.setSource(url, newTab=True)

    def __downloadMedia(self):
        """
        Private slot to download a media and save it to disk.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.DownloadMediaToDisk)

    def __pauseMedia(self):
        """
        Private slot to pause or play the selected media.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.ToggleMediaPlayPause)

    def __muteMedia(self):
        """
        Private slot to (un)mute the selected media.
        """
        self.triggerPageAction(QWebEnginePage.WebAction.ToggleMediaMute)

    def __virusTotal(self, act):
        """
        Private slot to scan the selected URL with VirusTotal.

        @param act reference to the action that triggered
        @type QAction
        """
        url = act.data()
        self.__mw.requestVirusTotalScan(url)

    def __searchDefaultRequested(self):
        """
        Private slot to search for some text with the current search engine.
        """
        searchText = self.selectedText()

        if not searchText:
            return

        engine = self.__mw.openSearchManager().currentEngine()
        if engine:
            self.search.emit(engine.searchUrl(searchText))

    def __searchRequested(self, act):
        """
        Private slot to search for some text with a selected search engine.

        @param act reference to the action that triggered this slot
        @type QAction
        """
        searchText = self.selectedText()

        if not searchText:
            return

        engineName = act.data()
        engine = (
            self.__mw.openSearchManager().engine(engineName)
            if engineName
            else self.__mw.openSearchManager().currentEngine()
        )
        if engine:
            self.search.emit(engine.searchUrl(searchText))

    def __addSearchEngine(self):
        """
        Private slot to add a new search engine.
        """
        from .Tools import Scripts

        script = Scripts.getFormData(self.__clickedPos)
        self.page().runJavaScript(
            script,
            WebBrowserPage.SafeJsWorld,
            lambda res: self.__mw.openSearchManager().addEngineFromForm(res, self),
        )

    def __webInspector(self):
        """
        Private slot to show the web inspector window.
        """
        from .WebInspector import WebInspector

        if WebInspector.isEnabled():
            if self.__inspector is None:
                self.__inspector = WebInspector()
                self.__inspector.setView(self, True)
                self.__inspector.inspectorClosed.connect(self.closeWebInspector)
                self.__inspector.show()
            else:
                self.closeWebInspector()

    def closeWebInspector(self):
        """
        Public slot to close the web inspector.
        """
        if self.__inspector is not None:
            if self.__inspector.isVisible():
                self.__inspector.hide()
            WebInspector.unregisterView(self.__inspector)
            self.__inspector.deleteLater()
            self.__inspector = None

    def addBookmark(self):
        """
        Public slot to bookmark the current page.
        """
        from .Tools import Scripts

        script = Scripts.getAllMetaAttributes()
        self.page().runJavaScript(
            script, WebBrowserPage.SafeJsWorld, self.__addBookmarkCallback
        )

    def __addBookmarkCallback(self, res):
        """
        Private callback method of __addBookmark().

        @param res reference to the result list containing all
            meta attributes
        @type list
        """
        from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog

        description = ""
        for meta in res:
            if meta["name"] == "description":
                description = meta["content"]

        dlg = AddBookmarkDialog(parent=self)
        dlg.setUrl(bytes(self.url().toEncoded()).decode())
        dlg.setTitle(self.title())
        dlg.setDescription(description)
        dlg.exec()

    def dragEnterEvent(self, evt):
        """
        Protected method called by a drag enter event.

        @param evt reference to the drag enter event
        @type QDragEnterEvent
        """
        evt.acceptProposedAction()

    def dragMoveEvent(self, evt):
        """
        Protected method called by a drag move event.

        @param evt reference to the drag move event
        @type QDragMoveEvent
        """
        evt.ignore()
        if evt.source() != self:
            if len(evt.mimeData().urls()) > 0:
                evt.acceptProposedAction()
            else:
                url = QUrl(evt.mimeData().text())
                if url.isValid():
                    evt.acceptProposedAction()

        if not evt.isAccepted():
            super().dragMoveEvent(evt)

    def dropEvent(self, evt):
        """
        Protected method called by a drop event.

        @param evt reference to the drop event
        @type QDropEvent
        """
        super().dropEvent(evt)
        if (
            not evt.isAccepted()
            and evt.source() != self
            and evt.possibleActions() & Qt.DropAction.CopyAction
        ):
            url = QUrl()
            if len(evt.mimeData().urls()) > 0:
                url = evt.mimeData().urls()[0]
            if not url.isValid():
                url = QUrl(evt.mimeData().text())
            if url.isValid():
                self.setSource(url)
                evt.acceptProposedAction()

    def _mousePressEvent(self, evt):
        """
        Protected method called by a mouse press event.

        @param evt reference to the mouse event
        @type QMouseEvent
        """
        if WebBrowserWindow.autoScroller().mousePress(self, evt):
            evt.accept()
            return

    def _mouseReleaseEvent(self, evt):
        """
        Protected method called by a mouse release event.

        @param evt reference to the mouse event
        @type QMouseEvent
        """
        if WebBrowserWindow.autoScroller().mouseRelease(evt):
            evt.accept()
            return

        accepted = evt.isAccepted()
        self.__page.event(evt)
        if not evt.isAccepted() and evt.button() == Qt.MouseButton.MiddleButton:
            url = QUrl(QApplication.clipboard().text(QClipboard.Mode.Selection))
            if not url.isEmpty() and url.isValid() and url.scheme() != "":
                self.setSource(url)
        evt.setAccepted(accepted)

    def _mouseMoveEvent(self, evt):
        """
        Protected method to handle mouse move events.

        @param evt reference to the mouse event
        @type QMouseEvent
        """
        if self.__mw and self.__mw.isFullScreen():
            if self.__mw.isFullScreenNavigationVisible():
                self.__mw.hideFullScreenNavigation()
            elif evt.y() < 10:
                # mouse is within 10px to the top
                self.__mw.showFullScreenNavigation()

        if WebBrowserWindow.autoScroller().mouseMove(evt):
            evt.accept()

    def _wheelEvent(self, evt):
        """
        Protected method to handle wheel events.

        @param evt reference to the wheel event
        @type QWheelEvent
        """
        if WebBrowserWindow.autoScroller().wheel():
            evt.accept()
            return

        delta = evt.angleDelta().y()
        if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
            if delta < 0:
                self.zoomOut()
            elif delta > 0:
                self.zoomIn()
            evt.accept()

        elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier:
            if delta < 0:
                self.backward()
            elif delta > 0:
                self.forward()
            evt.accept()

    def _keyPressEvent(self, evt):
        """
        Protected method called by a key press.

        @param evt reference to the key event
        @type QKeyEvent
        """
        if self.__mw.personalInformationManager().viewKeyPressEvent(self, evt):
            evt.accept()
            return

        if evt.key() == Qt.Key.Key_ZoomIn:
            self.zoomIn()
            evt.accept()
        elif evt.key() == Qt.Key.Key_ZoomOut:
            self.zoomOut()
            evt.accept()
        elif evt.key() == Qt.Key.Key_Plus:
            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
                self.zoomIn()
                evt.accept()
        elif evt.key() == Qt.Key.Key_Minus:
            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
                self.zoomOut()
                evt.accept()
        elif evt.key() == Qt.Key.Key_0:
            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
                self.zoomReset()
                evt.accept()
        elif evt.key() == Qt.Key.Key_M:
            if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
                self.__muteMedia()
                evt.accept()
        elif evt.key() == Qt.Key.Key_Backspace:
            pos = QCursor.pos()
            pos = self.mapFromGlobal(pos)
            hitTest = self.page().hitTestContent(pos)
            if not hitTest.isContentEditable():
                self.pageAction(QWebEnginePage.WebAction.Back).trigger()
                evt.accept()

    def _keyReleaseEvent(self, evt):
        """
        Protected method called by a key release.

        @param evt reference to the key event
        @type QKeyEvent
        """
        if evt.key() == Qt.Key.Key_Escape and self.isFullScreen():
            self.triggerPageAction(QWebEnginePage.WebAction.ExitFullScreen)
            evt.accept()
            self.requestFullScreen(False)

    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.__currentZoom / 100.0)
            elif pinch.state() == Qt.GestureState.GestureUpdated:
                scaleFactor = pinch.totalScaleFactor()
                self.setZoomValue(int(scaleFactor * 100))
            evt.accept()

    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.KeyRelease,
            QEvent.Type.MouseButtonPress,
            QEvent.Type.MouseButtonRelease,
            QEvent.Type.MouseMove,
            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.KeyRelease:
                self._keyReleaseEvent(evt)
            elif evt.type() == QEvent.Type.MouseButtonPress:
                self._mousePressEvent(evt)
            elif evt.type() == QEvent.Type.MouseButtonRelease:
                self._mouseReleaseEvent(evt)
            elif evt.type() == QEvent.Type.MouseMove:
                self._mouseMoveEvent(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)
            elif evt.type() == QEvent.Type.KeyRelease:
                self._keyReleaseEvent(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.KeyRelease,
                QEvent.Type.MouseButtonPress,
                QEvent.Type.MouseButtonRelease,
                QEvent.Type.MouseMove,
                QEvent.Type.Wheel,
                QEvent.Type.Gesture,
            ]:
                return True

            elif evt.type() == QEvent.Type.Hide and self.isFullScreen():
                self.triggerPageAction(QWebEnginePage.WebAction.ExitFullScreen)

        return super().eventFilter(obj, evt)

    def event(self, evt):
        """
        Public method handling events.

        @param evt reference to the event
        @type QEvent
        @return flag indicating, if the event was handled
        @rtype bool
        """
        if evt.type() == QEvent.Type.Gesture:
            self._gestureEvent(evt)
            return True

        return super().event(evt)

    def inputWidget(self):
        """
        Public method to get a reference to the render widget.

        @return reference to the render widget
        @rtype QWidget
        """
        return self.__rwhvqt

    def clearHistory(self):
        """
        Public slot to clear the history.
        """
        self.history().clear()
        self.__urlChanged(self.history().currentItem().url())

    ###########################################################################
    ## Signal converters below
    ###########################################################################

    def __urlChanged(self, url):
        """
        Private slot to handle the urlChanged signal.

        @param url the new url
        @type QUrl
        """
        self.sourceChanged.emit(url)

        self.forwardAvailable.emit(self.isForwardAvailable())
        self.backwardAvailable.emit(self.isBackwardAvailable())

    def __iconUrlChanged(self, url):
        """
        Private slot to handle the iconUrlChanged signal.

        @param url URL to get web site icon from
        @type QUrl
        """
        self.__siteIcon = QIcon()
        if self.__siteIconLoader is not None:
            self.__siteIconLoader.deleteLater()
        self.__siteIconLoader = WebIconLoader(url, self)
        self.__siteIconLoader.iconLoaded.connect(self.__iconLoaded)
        with contextlib.suppress(AttributeError):
            self.__siteIconLoader.sslConfiguration.connect(
                self.page().setSslConfiguration
            )
            self.__siteIconLoader.clearSslConfiguration.connect(
                self.page().clearSslConfiguration
            )

    def __iconLoaded(self, icon):
        """
        Private slot handling the loaded web site icon.

        @param icon web site icon
        @type QIcon
        """
        from eric7.WebBrowser.Tools import WebIconProvider

        self.__siteIcon = icon

        WebIconProvider.instance().saveIcon(self)

        self.faviconChanged.emit()

    def icon(self):
        """
        Public method to get the web site icon.

        @return web site icon
        @rtype QIcon
        """
        from eric7.WebBrowser.Tools import WebIconProvider

        if not self.__siteIcon.isNull():
            return QIcon(self.__siteIcon)

        return WebIconProvider.instance().iconForUrl(self.url())

    def title(self):
        """
        Public method to get the view title.

        @return view title
        @rtype str
        """
        titleStr = super().title()
        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 __linkHovered(self, link):
        """
        Private slot to handle the linkHovered signal.

        @param link the URL of the link
        @type str
        """
        self.highlighted.emit(link)

    ###########################################################################
    ## Signal handlers below
    ###########################################################################

    def __renderProcessTerminated(self, status, _exitCode):
        """
        Private slot handling a crash of the web page render process.

        @param status termination status
        @type QWebEnginePage.RenderProcessTerminationStatus
        @param _exitCode exit code of the process (unused)
        @type int
        """
        if (
            status
            == QWebEnginePage.RenderProcessTerminationStatus.NormalTerminationStatus
        ):
            return

        QTimer.singleShot(0, functools.partial(self.__showTabCrashPage, status))

    def __showTabCrashPage(self, status):
        """
        Private slot to show the tab crash page.

        @param status termination status
        @type QWebEnginePage.RenderProcessTerminationStatus
        """
        self.page().deleteLater()
        self.__createNewPage()

        html = getHtmlPage("tabCrashPage.html")
        html = html.replace(
            "@IMAGE@",
            pixmapToDataUrl(
                ericApp()
                .style()
                .standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
                .pixmap(48, 48)
            ).toString(),
        )
        html = html.replace(
            "@FAVICON@",
            pixmapToDataUrl(
                ericApp()
                .style()
                .standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
                .pixmap(16, 16)
            ).toString(),
        )
        html = html.replace("@TITLE@", self.tr("Render Process terminated abnormally"))
        html = html.replace("@H1@", self.tr("Render Process terminated abnormally"))
        if (
            status
            == QWebEnginePage.RenderProcessTerminationStatus.CrashedTerminationStatus
        ):
            msg = self.tr("The render process crashed while loading this page.")
        elif (
            status
            == QWebEnginePage.RenderProcessTerminationStatus.KilledTerminationStatus
        ):
            msg = self.tr("The render process was killed.")
        else:
            msg = self.tr("The render process terminated while loading this page.")
        html = html.replace("@LI-1@", msg)
        html = html.replace(
            "@LI-2@",
            self.tr(
                "Try reloading the page or closing some tabs to make more"
                " memory available."
            ),
        )
        self.page().setHtml(html, self.url())

    def __loadStarted(self):
        """
        Private method to handle the loadStarted signal.
        """
        # reset search
        self.findText("")
        self.__isLoading = True
        self.__progress = 0

    def __loadProgress(self, progress):
        """
        Private method to handle the loadProgress signal.

        @param progress progress value
        @type int
        """
        self.__progress = progress

    def __loadFinished(self, ok):
        """
        Private method to handle the loadFinished signal.

        @param ok flag indicating the result
        @type bool
        """
        self.__isLoading = False
        self.__progress = 0

        QApplication.processEvents()
        QTimer.singleShot(200, self.__renderPreview)

        zoomValue = ZoomManager.instance().zoomValue(self.url())
        self.setZoomValue(zoomValue)

        if ok:
            self.__mw.historyManager().addHistoryEntry(self)
            self.__mw.adBlockManager().page().hideBlockedPageEntries(self.page())
            self.__mw.passwordManager().completePage(self.page())

            self.page().runJavaScript(
                "document.lastModified",
                WebBrowserPage.SafeJsWorld,
                lambda res: self.__adjustBookmark(res),
            )

    def __adjustBookmark(self, lastModified):
        """
        Private slot to adjust the 'lastModified' value of bookmarks.

        @param lastModified last modified value
        @type str
        """
        from .Bookmarks.BookmarkNode import BookmarkTimestampType

        modified = QDateTime.fromString(lastModified, "MM/dd/yyyy hh:mm:ss")
        if modified.isValid():
            manager = WebBrowserWindow.bookmarksManager()
            for bookmark in manager.bookmarksForUrl(self.url()):
                manager.setTimestamp(bookmark, BookmarkTimestampType.Modified, modified)

    def isLoading(self):
        """
        Public method to get the loading state.

        @return flag indicating the loading state
        @rtype bool
        """
        return self.__isLoading

    def progress(self):
        """
        Public method to get the load progress.

        @return load progress
        @rtype int
        """
        return self.__progress

    def __renderPreview(self):
        """
        Private slot to render a preview pixmap after the page was loaded.
        """
        from .WebBrowserSnap import renderTabPreview

        w = 600  # some default width, the preview gets scaled when shown
        h = int(w * self.height() / self.width())
        self.__preview = renderTabPreview(self, w, h)

    def getPreview(self):
        """
        Public method to get the preview pixmap.

        @return preview pixmap
        @rtype QPixmap
        """
        return self.__preview

    def saveAs(self):
        """
        Public method to save the current page to a file.
        """
        url = self.url()
        if url.isEmpty():
            return

        fileName, savePageFormat = self.__getSavePageFileNameAndFormat()
        if fileName:
            self.page().save(fileName, savePageFormat)

    def __getSavePageFileNameAndFormat(self):
        """
        Private method to get the file name to save the page to.

        @return tuple containing the file name to save to and the
            save page format
        @rtype tuple of (str, QWebEngineDownloadRequest.SavePageFormat)
        """
        documentLocation = QStandardPaths.writableLocation(
            QStandardPaths.StandardLocation.DocumentsLocation
        )
        filterList = [
            self.tr("Web Archive (*.mhtml *.mht)"),
            self.tr("HTML File (*.html *.htm)"),
            self.tr("HTML File with all resources (*.html *.htm)"),
        ]
        extensionsList = [
            # tuple of extensions for *nix and Windows
            # keep in sync with filters list
            (".mhtml", ".mht"),
            (".html", ".htm"),
            (".html", ".htm"),
        ]
        if self.url().fileName():
            defaultFileName = os.path.join(documentLocation, self.url().fileName())
        else:
            defaultFileName = os.path.join(documentLocation, self.page().title())
            if OSUtilities.isWindowsPlatform():
                defaultFileName += ".mht"
            else:
                defaultFileName += ".mhtml"

        fileName = ""
        saveFormat = QWebEngineDownloadRequest.SavePageFormat.MimeHtmlSaveFormat

        fileName, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
            None, self.tr("Save Web Page"), defaultFileName, ";;".join(filterList), None
        )
        if fileName:
            index = filterList.index(selectedFilter)
            if index == 0:
                saveFormat = QWebEngineDownloadRequest.SavePageFormat.MimeHtmlSaveFormat
            elif index == 1:
                saveFormat = (
                    QWebEngineDownloadRequest.SavePageFormat.SingleHtmlSaveFormat
                )
            else:
                saveFormat = (
                    QWebEngineDownloadRequest.SavePageFormat.CompleteHtmlSaveFormat
                )

            extension = os.path.splitext(fileName)[1]
            if not extension:
                # add the platform specific default extension
                if OSUtilities.isWindowsPlatform():
                    extensionsIndex = 1
                else:
                    extensionsIndex = 0
                extensions = extensionsList[index]
                fileName += extensions[extensionsIndex]

        return fileName, saveFormat

    @pyqtSlot("QWebEngineClientCertificateSelection")
    def __selectClientCertificate(self, clientCertificateSelection):
        """
        Private slot to handle the client certificate selection request.

        @param clientCertificateSelection list of client SSL certificates
            found in system's client certificate store
        @type QWebEngineClientCertificateSelection
        """
        from eric7.EricNetwork.EricSslCertificateSelectionDialog import (
            EricSslCertificateSelectionDialog,
        )

        certificates = clientCertificateSelection.certificates()
        if len(certificates) == 0:
            clientCertificateSelection.selectNone()
        elif len(certificates) == 1:
            clientCertificateSelection.select(certificates[0])
        else:
            certificate = None
            dlg = EricSslCertificateSelectionDialog(certificates, parent=self)
            if dlg.exec() == QDialog.DialogCode.Accepted:
                certificate = dlg.getSelectedCertificate()

            if certificate is None:
                clientCertificateSelection.selectNone()
            else:
                clientCertificateSelection.select(certificate)

    ###########################################################################
    ## Miscellaneous methods below
    ###########################################################################

    def createWindow(self, windowType):
        """
        Public method called, when a new window should be created.

        @param windowType type of the requested window
        @type QWebEnginePage.WebWindowType
        @return reference to the created browser window
        @rtype WebBrowserView
        """
        if windowType in [
            QWebEnginePage.WebWindowType.WebBrowserTab,
            QWebEnginePage.WebWindowType.WebDialog,
        ]:
            return self.__mw.newTab(addNextTo=self)
        elif windowType == QWebEnginePage.WebWindowType.WebBrowserWindow:
            return self.__mw.newWindow().currentBrowser()
        elif windowType == QWebEnginePage.WebWindowType.WebBrowserBackgroundTab:
            return self.__mw.newTab(addNextTo=self, background=True)
        else:
            # default for unknow/new window types
            return self.__mw.newTab(addNextTo=self)

    def preferencesChanged(self):
        """
        Public method to indicate a change of the settings.
        """
        self.reload()

    ###########################################################################
    ## RSS related methods below
    ###########################################################################

    def checkRSS(self):
        """
        Public method to check, if the loaded page contains feed links.

        @return flag indicating the existence of feed links
        @rtype bool
        """
        self.__rss = []

        script = Scripts.getFeedLinks()
        feeds = self.page().execJavaScript(script)

        if feeds is not None:
            for feed in feeds:
                if feed["url"] and feed["title"]:
                    self.__rss.append((feed["title"], feed["url"]))

        return len(self.__rss) > 0

    def getRSS(self):
        """
        Public method to get the extracted RSS feeds.

        @return list of RSS feeds
        @rtype list of [(str, str)]
        """
        return self.__rss

    def hasRSS(self):
        """
        Public method to check, if the loaded page has RSS links.

        @return flag indicating the presence of RSS links
        @rtype bool
        """
        return len(self.__rss) > 0

    ###########################################################################
    ## Full Screen handling below
    ###########################################################################

    def isFullScreen(self):
        """
        Public method to check, if full screen mode is active.

        @return flag indicating full screen mode
        @rtype bool
        """
        return self.__mw.isFullScreen()

    def requestFullScreen(self, enable):
        """
        Public method to request full screen mode.

        @param enable flag indicating full screen mode on or off
        @type bool
        """
        if enable:
            self.__mw.enterHtmlFullScreen()
        else:
            self.__mw.showNormal()

    ###########################################################################
    ## Speed Dial slots below
    ###########################################################################

    def __addSpeedDial(self):
        """
        Private slot to add a new speed dial.
        """
        self.__page.runJavaScript("addSpeedDial();", WebBrowserPage.UnsafeJsWorld)

    def __configureSpeedDial(self):
        """
        Private slot to configure the speed dial.
        """
        self.page().runJavaScript("configureSpeedDial();", WebBrowserPage.UnsafeJsWorld)

    def __reloadAllSpeedDials(self):
        """
        Private slot to reload all speed dials.
        """
        self.page().runJavaScript("reloadAll();", WebBrowserPage.UnsafeJsWorld)

    def __resetSpeedDials(self):
        """
        Private slot to reset all speed dials to the default pages.
        """
        self.__speedDial.resetDials()

    ###########################################################################
    ## Methods below implement session related functions
    ###########################################################################

    def storeSessionData(self, data):
        """
        Public method to store session data to be restored later on.

        @param data dictionary with session data to be restored
        @type dict
        """
        self.__restoreData = data

    def __showEventSlot(self):
        """
        Private slot to perform actions when the view is shown and the event
        loop is running.
        """
        if self.__restoreData:
            sessionData, self.__restoreData = self.__restoreData, None
            self.loadFromSessionData(sessionData)

    def showEvent(self, evt):
        """
        Protected method to handle show events.

        @param evt reference to the show event object
        @type QShowEvent
        """
        super().showEvent(evt)
        self.activateSession()

    def activateSession(self):
        """
        Public slot to activate a restored session.
        """
        if self.__restoreData and not self.__mw.isClosing():
            QTimer.singleShot(0, self.__showEventSlot)

    def getSessionData(self):
        """
        Public method to populate the session data.

        @return dictionary containing the session data
        @rtype dict
        """
        if self.__restoreData:
            # page has not been shown yet
            return self.__restoreData

        sessionData = {}
        page = self.page()

        # 1. zoom factor
        sessionData["ZoomFactor"] = page.zoomFactor()

        # 2. scroll position
        scrollPos = page.scrollPosition()
        sessionData["ScrollPosition"] = {
            "x": scrollPos.x(),
            "y": scrollPos.y(),
        }

        # 3. page history
        historyArray = QByteArray()
        stream = QDataStream(historyArray, QIODevice.OpenModeFlag.WriteOnly)
        stream << page.history()
        sessionData["History"] = str(
            historyArray.toBase64(QByteArray.Base64Option.Base64UrlEncoding),
            encoding="ascii",
        )
        sessionData["HistoryIndex"] = page.history().currentItemIndex()

        # 4. current URL and title
        sessionData["Url"] = self.url().toString()
        sessionData["Title"] = self.title()

        # 5. web icon
        iconArray = QByteArray()
        stream = QDataStream(iconArray, QIODevice.OpenModeFlag.WriteOnly)
        stream << page.icon()
        sessionData["Icon"] = str(iconArray.toBase64(), encoding="ascii")

        return sessionData

    def loadFromSessionData(self, sessionData):
        """
        Public method to load the session data.

        @param sessionData dictionary containing the session data as
            generated by getSessionData()
        @type dict
        """
        page = self.page()
        # blank the page
        page.setUrl(QUrl("about:blank"))

        # 1. page history
        if "History" in sessionData:
            historyArray = QByteArray.fromBase64(
                sessionData["History"].encode("ascii"),
                QByteArray.Base64Option.Base64UrlEncoding,
            )
            stream = QDataStream(historyArray, QIODevice.OpenModeFlag.ReadOnly)
            stream >> page.history()

            if "HistoryIndex" in sessionData:
                item = page.history().itemAt(sessionData["HistoryIndex"])
                if item is not None:
                    page.history().goToItem(item)

        # 2. zoom factor
        if "ZoomFactor" in sessionData:
            page.setZoomFactor(sessionData["ZoomFactor"])

        # 3. scroll position
        if "ScrollPosition" in sessionData:
            scrollPos = sessionData["ScrollPosition"]
            page.scrollTo(QPointF(scrollPos["x"], scrollPos["y"]))

    def extractSessionMetaData(self, sessionData):
        """
        Public method to extract some session meta data elements needed by the
        tab widget in case of deferred loading.

        @param sessionData dictionary containing the session data as
            generated by getSessionData()
        @type dict
        @return tuple containing the title, URL and web icon
        @rtype tuple of (str, str, QIcon)
        """
        from eric7.WebBrowser.Tools import WebIconProvider

        title = sessionData.get("Title", "")
        urlStr = sessionData.get("Url", "")

        if "Icon" in sessionData:
            iconArray = QByteArray.fromBase64(sessionData["Icon"].encode("ascii"))
            stream = QDataStream(iconArray, QIODevice.OpenModeFlag.ReadOnly)
            icon = QIcon()
            stream >> icon
        else:
            icon = WebIconProvider.instance().iconForUrl(QUrl.fromUserInput(urlStr))

        return title, urlStr, icon

    ###########################################################################
    ## Methods below implement safe browsing related functions
    ###########################################################################

    def getSafeBrowsingStatus(self):
        """
        Public method to get the safe browsing status of the current page.

        @return flag indicating a safe site
        @rtype bool
        """
        if self.__page:
            return self.__page.getSafeBrowsingStatus()
        else:
            return True

    ###########################################################################
    ## Methods below implement print support
    ###########################################################################

    def __setupPrinter(self, filePath=None):
        """
        Private method to create and initialize a QPrinter object.

        @param filePath name of the output file for the printer (defaults to None)
        @type str (optional)
        @return initialized QPrinter object
        @rtype QPrinter
        """
        printer = QPrinter(mode=QPrinter.PrinterMode.HighResolution)
        if Preferences.getPrinter("ColorMode"):
            printer.setColorMode(QPrinter.ColorMode.Color)
        else:
            printer.setColorMode(QPrinter.ColorMode.GrayScale)
        if Preferences.getPrinter("FirstPageFirst"):
            printer.setPageOrder(QPrinter.PageOrder.FirstPageFirst)
        else:
            printer.setPageOrder(QPrinter.PageOrder.LastPageFirst)
        printer.setPageMargins(
            QMarginsF(
                Preferences.getPrinter("LeftMargin") * 10,
                Preferences.getPrinter("TopMargin") * 10,
                Preferences.getPrinter("RightMargin") * 10,
                Preferences.getPrinter("BottomMargin") * 10,
            ),
            QPageLayout.Unit.Millimeter,
        )
        printerName = Preferences.getPrinter("PrinterName")
        if printerName:
            printer.setPrinterName(printerName)
        printer.setResolution(Preferences.getPrinter("Resolution"))
        documentName = WebBrowserTools.getFileNameFromUrl(self.url())
        printer.setDocName(documentName)
        documentsPath = QStandardPaths.writableLocation(
            QStandardPaths.StandardLocation.DocumentsLocation
        )
        if filePath is None:
            filePath = "{0}.pdf".format(documentName)
        filePath = (
            os.path.join(documentsPath, filePath)
            if documentsPath
            else os.path.abspath(filePath)
        )
        printer.setOutputFileName(filePath)
        printer.setCreator(self.tr("eric7 {0} ({1})").format(VersionOnly, Homepage))
        return printer

    @pyqtSlot()
    def printPage(self):
        """
        Public slot to print the current page.
        """
        if self.__currentPrinter is not None:
            EricMessageBox.warning(
                self,
                self.tr("Print Page"),
                self.tr(
                    "There is already a print job in progress. Printing is temporarily"
                    " disabled until the current job is finished."
                ),
            )
            return

        printer = self.__setupPrinter()

        printDialog = QPrintDialog(printer, self)
        printDialog.setOptions(
            QAbstractPrintDialog.PrintDialogOption.PrintToFile
            | QAbstractPrintDialog.PrintDialogOption.PrintShowPageSize
        )
        if not OSUtilities.isWindowsPlatform():
            if isCupsAvailable():
                printDialog.setOption(
                    QAbstractPrintDialog.PrintDialogOption.PrintCollateCopies
                )
            printDialog.setOption(QAbstractPrintDialog.PrintDialogOption.PrintPageRange)
        if printDialog.exec() == QDialog.DialogCode.Accepted:
            if printer.outputFormat() == QPrinter.OutputFormat.PdfFormat:
                self.printToPdf(
                    printer.outputFileName(), printer.pageLayout(), printer.pageRanges()
                )
            else:
                self.__currentPrinter = printer
                self.print(printer)

    @pyqtSlot()
    def printPageToPdf(self):
        """
        Public slot to save the current page as a PDF file.
        """
        from .Tools.PrintToPdfDialog import PrintToPdfDialog

        name = WebBrowserTools.getFileNameFromUrl(self.url())
        name = name.rsplit(".", 1)[0] + ".pdf" if name else "printout.pdf"
        dlg = PrintToPdfDialog(self.__setupPrinter(filePath=name), parent=self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            filePath, pageLayout = dlg.getData()
            if filePath:
                if os.path.exists(filePath):
                    res = EricMessageBox.warning(
                        self,
                        self.tr("Print to PDF"),
                        self.tr(
                            """<p>The file <b>{0}</b> exists"""
                            """ already. Shall it be"""
                            """ overwritten?</p>"""
                        ).format(filePath),
                        EricMessageBox.No | EricMessageBox.Yes,
                        EricMessageBox.No,
                    )
                    if res == EricMessageBox.No:
                        return
                self.printToPdf(filePath, pageLayout)

    @pyqtSlot()
    def printPreviewPage(self):
        """
        Public slot to create a print preview of the current page.
        """
        printer = self.__setupPrinter()
        preview = QPrintPreviewDialog(printer, self)
        preview.resize(800, 750)
        preview.paintRequested.connect(self.__printPreviewRequested)
        preview.exec()

    @pyqtSlot(QPrinter)
    def __printPreviewRequested(self, printer):
        """
        Private slot to generate the print preview.

        @param printer reference to the printer object
        @type QPrinter
        """
        # This needs to run its own event loop to prevent a premature return from
        # the method.
        self.__printPreviewLoop = QEventLoop()

        self.print(printer)

        self.__printPreviewLoop.exec()
        self.__printPreviewLoop = None

    @pyqtSlot(bool)
    def __printPageFinished(self, success):
        """
        Private slot to handle the finishing of a print job.

        @param success flag indicating success (not used)
        @type bool
        """
        if self.__printPreviewLoop is not None:
            # The print preview was created, now stop the print preview loop.
            self.__printPreviewLoop.quit()
            return

        # we printed to a real printer
        self.__currentPrinter = None

    @pyqtSlot(str, bool)
    def __printPageToPdfFinished(self, filepath, success):
        """
        Private slot to handle the finishing of a PDF print job.

        @param filepath path of the output PDF file
        @type str
        @param success flag indicating success
        @type bool
        """
        if not success:
            EricMessageBox.critical(
                self,
                self.tr("Print to PDF"),
                self.tr(
                    """<p>The PDF file <b>{0}</b> could not be generated.</p>"""
                ).format(filepath),
            )

    ###########################################################################
    ## Methods below implement slots for Qt 6.0 to 6.4
    ###########################################################################

    # deprecated with Qt 6.5+
    @pyqtSlot("QWebEngineQuotaRequest")
    def __quotaRequested(self, quotaRequest):
        """
        Private slot to handle quota requests of the web page.

        @param quotaRequest reference to the quota request object
        @type QWebEngineQuotaRequest
        """
        from .Download.DownloadUtilities import dataString

        acceptRequest = Preferences.getWebBrowser("AcceptQuotaRequest")
        # map yes/no/ask from (0, 1, 2)
        if acceptRequest == 0:
            # always yes
            ok = True
        elif acceptRequest == 1:
            # always no
            ok = False
        else:
            # ask user
            sizeStr = dataString(quotaRequest.requestedSize())

            ok = EricMessageBox.yesNo(
                self,
                self.tr("Quota Request"),
                self.tr(
                    """<p> Allow the website at <b>{0}</b> to use"""
                    """ <b>{1}</b> of persistent storage?</p>"""
                ).format(quotaRequest.origin().host(), sizeStr),
            )

        if ok:
            quotaRequest.accept()
        else:
            quotaRequest.reject()

    ###########################################################################
    ## Methods below implement slots for Qt 6.4+
    ###########################################################################

    with contextlib.suppress(TypeError):

        @pyqtSlot("QWebEngineFileSystemAccessRequest")
        def __fileSystemAccessRequested(self, accessRequest):
            """
            Private slot to handle file system access requests of the web page.

            @param accessRequest reference to the file system access request object
            @type QWebEngineFileSystemAccessRequest
            """
            from PyQt6.QtWebEngineCore import (  # __IGNORE_WARNING_I-102__
                QWebEngineFileSystemAccessRequest,
            )

            acceptRequest = Preferences.getWebBrowser("AcceptFileSystemAccessRequest")
            # map yes/no/ask from (0, 1, 2)
            if acceptRequest == 0:
                # always yes
                ok = True
            elif acceptRequest == 1:
                # always no
                ok = False
            else:
                # ask user
                if (
                    accessRequest.accessFlags()
                    == QWebEngineFileSystemAccessRequest.AccessFlag.Read
                ):
                    msgTemplate = self.tr(
                        "<p>Grant the website at <b>{0}</b> <b>Read</b> access"
                        " to '{1}'?</p>"
                    )
                elif (
                    accessRequest.accessFlags()
                    == QWebEngineFileSystemAccessRequest.AccessFlag.Write
                ):
                    msgTemplate = self.tr(
                        "<p>Grant the website at <b>{0}</b> <b>Write</b> access"
                        " to '{1}'?</p>"
                    )
                else:
                    msgTemplate = self.tr(
                        "<p>Grant the website at <b>{0}</b> <b>Read and Write</b>"
                        " access to '{1}'?</p>"
                    )

                ok = EricMessageBox.yesNo(
                    self,
                    self.tr("File System Access Request"),
                    msgTemplate.format(
                        accessRequest.origin().host(),
                        accessRequest.filePath().toLocalFile(),
                    ),
                )

            if ok:
                accessRequest.accept()
            else:
                accessRequest.reject()

    ###########################################################################
    ## Methods below implement slots for Qt 6.7+
    ###########################################################################

    with contextlib.suppress(TypeError):

        @pyqtSlot("QWebEngineWebAuthUxRequest*")
        def __webAuthUxRequested(self, authUxRequest):
            """
            Private slot to handle WebAuth requests.

            @param authUxRequest reference to the WebAuth request object
            @type QWebEngineWebAuthUxRequest
            """
            from .WebAuth.WebBrowserWebAuthDialog import WebBrowserWebAuthDialog

            if self.__webAuthDialog is not None:
                del self.__webAuthDialog
                self.__webAuthDialog = None

            self.__webAuthDialog = WebBrowserWebAuthDialog(authUxRequest, self.__mw)
            self.__webAuthDialog.setModal(False)
            self.__webAuthDialog.setWindowFlags(
                self.__webAuthDialog.windowFlags()
                & ~Qt.WindowType.WindowContextHelpButtonHint
            )

            authUxRequest.stateChanged.connect(self.__webAuthUxRequestStateChanged)
            self.__webAuthDialog.show()

    with contextlib.suppress(TypeError):

        @pyqtSlot("QWebEngineWebAuthUxRequest::WebAuthUxState")
        def __webAuthUxRequestStateChanged(self, state):
            """
            Private slot to handle a change of state of the current WebAuth request.

            @param state new state
            @type QWebEngineWebAuthUxRequest.WebAuthUxState
            """
            if state in (
                QWebEngineWebAuthUxRequest.WebAuthUxState.Cancelled,
                QWebEngineWebAuthUxRequest.WebAuthUxState.Completed,
            ):
                self.__webAuthDialog.hide()
                del self.__webAuthDialog
                self.__webAuthDialog = None
            else:
                self.__webAuthDialog.updateDialog()
                self.__webAuthDialog.show()

eric ide

mercurial