diff -r 4e8b98454baa -r 800c432b34c8 eric7/WebBrowser/WebBrowserView.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/WebBrowser/WebBrowserView.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,2343 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2008 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing the web browser using QWebEngineView. +""" + +import os +import functools +import contextlib + +from PyQt5.QtCore import ( + pyqtSignal, pyqtSlot, Qt, QUrl, QFileInfo, QTimer, QEvent, QPoint, + QPointF, QDateTime, QStandardPaths, QByteArray, QIODevice, QDataStream +) +from PyQt5.QtGui import ( + QDesktopServices, QClipboard, QIcon, QContextMenuEvent, QPixmap, QCursor +) +from PyQt5.QtWidgets import QStyle, QMenu, QApplication, QDialog +from PyQt5.QtWebEngineWidgets import ( + QWebEngineView, QWebEnginePage, QWebEngineDownloadItem +) + +from E5Gui import E5MessageBox, E5FileDialog +from E5Gui.E5Application import e5App + +from WebBrowser.WebBrowserWindow import WebBrowserWindow +from .WebBrowserPage import WebBrowserPage + +from .Tools.WebIconLoader import WebIconLoader +from .Tools import Scripts + +from . import WebInspector +from .Tools.WebBrowserTools import getHtmlPage, pixmapToDataUrl + +import Preferences +import UI.PixmapCache +import Utilities + + +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 (WebBrowserWindow) + @param parent parent widget of this window (QWidget) + @param name name of this window (string) + """ + 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.__currentZoom = 100 + self.__zoomLevels = WebBrowserView.ZoomLevels[:] + + 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.__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.setPage(self.__page) + + self.__page.safeBrowsingAbort.connect(self.safeBrowsingAbort) + self.__page.safeBrowsingBad.connect(self.safeBrowsingBad) + self.__page.printPageRequested.connect(self.__printPage) + self.__page.quotaRequested.connect(self.__quotaRequested) + # The registerProtocolHandlerRequested signal is handled in + # WebBrowserPage. + self.__page.selectClientCertificate.connect( + self.__selectClientCertificate) + with contextlib.suppress(AttributeError, ImportError): + #- Qt >= 5.14 + from PyQt5.QtWebEngineCore import QWebEngineFindTextResult + # __IGNORE_WARNING__ + + 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_M801__ + + 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 (QUrl) + @param newTab flag indicating to open the URL in a new tab (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 Utilities.isWindowsPlatform(): + name.setUrl("file:///" + Utilities.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 QFileInfo(name.toLocalFile()).exists(): + E5MessageBox.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: + E5MessageBox.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: + E5MessageBox.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: + E5MessageBox.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 (QUrl) + """ + return self.url() + + def documentTitle(self): + """ + Public method to return the title of the loaded page. + + @return title (string) + """ + 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. + """ + try: + self.triggerPageAction(QWebEnginePage.WebAction.Unselect) + except AttributeError: + # prior to 5.7.0 + self.page().runJavaScript( + "window.getSelection().empty()", + WebBrowserPage.SafeJsWorld) + + def isForwardAvailable(self): + """ + Public method to determine, if a forward move in history is possible. + + @return flag indicating move forward is possible (boolean) + """ + 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 (boolean) + """ + return self.history().canGoBack() + + def __levelForZoom(self, zoom): + """ + Private method determining the zoom level index given a zoom factor. + + @param zoom zoom factor (integer) + @return index of zoom factor (integer) + """ + 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 (integer) + @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(): + from .ZoomManager import ZoomManager + ZoomManager.instance().setZoomValue(self.url(), value) + self.zoomValueChanged.emit(value) + + def zoomValue(self): + """ + Public method to get the current zoom value. + + @return zoom value (integer) + """ + 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 (boolean) + """ + 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 (string) + @param case flag indicating a case sensitive search (boolean) + @param backwards flag indicating a backwards search (boolean) + @param callback reference to a function with a bool parameter + @type function(bool) or None + """ + findFlags = QWebEnginePage.FindFlags() + 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()) + ) + + def contextMenuEvent(self, evt): + """ + Protected method called to create a context menu. + + This method is overridden from QWebEngineView. + + @param evt reference to the context menu event object + (QContextMenuEvent) + """ + pos = evt.pos() + reason = evt.reason() + QTimer.singleShot( + 0, + lambda: self._contextMenuEvent(QContextMenuEvent(reason, pos))) + # needs to be done this way because contextMenuEvent is blocking + # the main loop + + def _contextMenuEvent(self, evt): + """ + Protected method called to create a context menu. + + This method is overridden from QWebEngineView. + + @param evt reference to the context menu event object + (QContextMenuEvent) + """ + self.__menu.clear() + + hitTest = self.page().hitTestContent(evt.pos()) + + self.__createContextMenu(self.__menu, hitTest) + + if not hitTest.isContentEditable() and not hitTest.isContentSelected(): + self.__menu.addSeparator() + self.__menu.addAction(self.__mw.adBlockIcon().menuAction()) + + self.__menu.addSeparator() + self.__menu.addAction( + UI.PixmapCache.getIcon("webInspector"), + self.tr("Inspect Element..."), self.__webInspector) + + if not self.__menu.isEmpty(): + pos = evt.globalPos() + self.__menu.popup(QPoint(pos.x(), pos.y() + 1)) + + 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.page().contextMenuData() + 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( + UI.PixmapCache.getIcon("openNewTab"), + self.tr("Open Link in New Tab\tCtrl+LMB")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: self.__openLinkInNewTab(act)) + act = menu.addAction( + UI.PixmapCache.getIcon("newWindow"), + self.tr("Open Link in New Window")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: self.__openLinkInNewWindow(act)) + act = menu.addAction( + UI.PixmapCache.getIcon("privateMode"), + self.tr("Open Link in New Private Window")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: self.__openLinkInNewPrivateWindow(act)) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("download"), + self.tr("Save Lin&k"), self.__downloadLink) + act = menu.addAction( + UI.PixmapCache.getIcon("bookmark22"), + self.tr("Bookmark this Link")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: self.__bookmarkLink(act)) + menu.addSeparator() + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy Link to Clipboard")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: self.__copyLink(act)) + act = menu.addAction( + UI.PixmapCache.getIcon("mailSend"), + self.tr("Send Link")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: self.__sendLink(act)) + if ( + Preferences.getWebBrowser("VirusTotalEnabled") and + Preferences.getWebBrowser("VirusTotalServiceKey") != "" + ): + act = menu.addAction( + UI.PixmapCache.getIcon("virustotal"), + self.tr("Scan Link with VirusTotal")) + act.setData(hitTest.linkUrl()) + act.triggered.connect( + lambda: 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( + UI.PixmapCache.getIcon("openNewTab"), + self.tr("Open Image in New Tab")) + act.setData(hitTest.imageUrl()) + act.triggered.connect( + lambda: self.__openLinkInNewTab(act)) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("download"), + self.tr("Save Image"), self.__downloadImage) + menu.addAction( + self.tr("Copy Image to Clipboard"), self.__copyImage) + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy Image Location to Clipboard")) + act.setData(hitTest.imageUrl()) + act.triggered.connect( + lambda: self.__copyLink(act)) + act = menu.addAction( + UI.PixmapCache.getIcon("mailSend"), + self.tr("Send Image Link")) + act.setData(hitTest.imageUrl()) + act.triggered.connect( + lambda: self.__sendLink(act)) + + if hitTest.imageUrl().scheme() in ["http", "https"]: + menu.addSeparator() + engine = WebBrowserWindow.imageSearchEngine() + searchEngineName = engine.searchEngine() + act = menu.addAction( + UI.PixmapCache.getIcon("{0}".format( + searchEngineName.lower())), + self.tr("Search image in {0}").format(searchEngineName)) + act.setData(engine.getSearchQuery(hitTest.imageUrl())) + act.triggered.connect( + lambda: self.__searchImage(act)) + self.__imageSearchMenu = menu.addMenu( + self.tr("Search image with...")) + for searchEngineName in engine.searchEngineNames(): + act = self.__imageSearchMenu.addAction( + UI.PixmapCache.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( + UI.PixmapCache.getIcon("adBlockPlus"), + self.tr("Block Image")) + act.setData(hitTest.imageUrl().toString()) + act.triggered.connect( + lambda: self.__blockImage(act)) + if ( + Preferences.getWebBrowser("VirusTotalEnabled") and + Preferences.getWebBrowser("VirusTotalServiceKey") != "" + ): + act = menu.addAction( + UI.PixmapCache.getIcon("virustotal"), + self.tr("Scan Image with VirusTotal")) + act.setData(hitTest.imageUrl()) + act.triggered.connect( + lambda: 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( + UI.PixmapCache.getIcon("mediaPlaybackStart"), + self.tr("Play"), self.__pauseMedia) + else: + menu.addAction( + UI.PixmapCache.getIcon("mediaPlaybackPause"), + self.tr("Pause"), self.__pauseMedia) + if hitTest.mediaMuted(): + menu.addAction( + UI.PixmapCache.getIcon("audioVolumeHigh"), + self.tr("Unmute"), self.__muteMedia) + else: + menu.addAction( + UI.PixmapCache.getIcon("audioVolumeMuted"), + self.tr("Mute"), self.__muteMedia) + menu.addSeparator() + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy Media Address to Clipboard")) + act.setData(hitTest.mediaUrl()) + act.triggered.connect( + lambda: self.__copyLink(act)) + act = menu.addAction( + UI.PixmapCache.getIcon("mailSend"), + self.tr("Send Media Address")) + act.setData(hitTest.mediaUrl()) + act.triggered.connect( + lambda: self.__sendLink(act)) + menu.addAction( + UI.PixmapCache.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 + @type WebHitTestResult + """ + if not menu.isEmpty(): + menu.addSeparator() + + menu.addAction(self.__mw.copyAct) + menu.addSeparator() + act = menu.addAction( + UI.PixmapCache.getIcon("mailSend"), + self.tr("Send Text")) + act.setData(self.selectedText()) + act.triggered.connect( + lambda: self.__sendLink(act)) + + engineName = self.__mw.openSearchManager().currentEngineName() + if engineName: + menu.addAction(self.tr("Search with '{0}'").format(engineName), + self.__searchDefaultRequested) + + from .OpenSearch.OpenSearchEngineAction import ( + OpenSearchEngineAction + ) + + 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() + + from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog + languages = Preferences.toList( + Preferences.Prefs.settings.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())) + act1 = menu.addAction( + UI.PixmapCache.getIcon("translate"), + self.tr("Google Translate")) + act1.setData(googleTranslatorUrl) + act1.triggered.connect( + lambda: self.__openLinkInNewTab(act1)) + wiktionaryUrl = QUrl( + "http://{0}.wiktionary.org/wiki/Special:Search?search={1}" + .format(langCode, self.selectedText())) + act2 = menu.addAction( + UI.PixmapCache.getIcon("wikipedia"), + self.tr("Dictionary")) + act2.setData(wiktionaryUrl) + act2.triggered.connect( + lambda: self.__openLinkInNewTab(act2)) + menu.addSeparator() + + guessedUrl = QUrl.fromUserInput(self.selectedText().strip()) + if self.__isUrlValid(guessedUrl): + act3 = menu.addAction(self.tr("Go to web address")) + act3.setData(guessedUrl) + act3.triggered.connect( + lambda: self.__openLinkInNewTab(act3)) + + def __createPageContextMenu(self, menu): + """ + Private method to populate the basic context menu. + + @param menu reference to the menu to be populated + @type QMenu + """ + 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( + UI.PixmapCache.getIcon("plus"), + self.tr("Add New Page"), self.__addSpeedDial) + menu.addAction( + UI.PixmapCache.getIcon("preferences-general"), + self.tr("Configure Speed Dial"), self.__configureSpeedDial) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("reload"), + self.tr("Reload All Dials"), self.__reloadAllSpeedDials) + menu.addSeparator() + menu.addAction( + self.tr("Reset to Default Dials"), self.__resetSpeedDials) + return + + menu.addAction( + UI.PixmapCache.getIcon("bookmark22"), + self.tr("Bookmark this Page"), self.addBookmark) + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy Page Link")) + act.setData(self.url()) + act.triggered.connect( + lambda: self.__copyLink(act)) + act = menu.addAction( + UI.PixmapCache.getIcon("mailSend"), + self.tr("Send Page Link")) + act.setData(self.url()) + act.triggered.connect( + lambda: self.__sendLink(act)) + menu.addSeparator() + + from .UserAgent.UserAgentMenu import UserAgentMenu + 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())) + act1 = menu.addAction( + UI.PixmapCache.getIcon("w3"), + self.tr("Validate Page")) + act1.setData(w3url) + act1.triggered.connect( + lambda: self.__openLinkInNewTab(act1)) + + from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog + languages = Preferences.toList( + Preferences.Prefs.settings.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())) + act2 = menu.addAction( + UI.PixmapCache.getIcon("translate"), + self.tr("Google Translate")) + act2.setData(googleTranslatorUrl) + act2.triggered.connect( + lambda: self.__openLinkInNewTab(act2)) + + 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 + """ + self.__clickedPos = self.mapToViewport(pos) + + from .Tools import Scripts + 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 (QUrl) + @return flag indicating a valid URL (boolean) + """ + 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 + """ + url = act.data() + if url.isEmpty(): + return + + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + 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() + QApplication.clipboard().setText(data) + + 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 (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 + """ + description = "" + for meta in res: + if meta["name"] == "description": + description = meta["content"] + + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + 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 (QDragEnterEvent) + """ + evt.acceptProposedAction() + + def dragMoveEvent(self, evt): + """ + Protected method called by a drag move event. + + @param evt reference to the drag move event (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 (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 (QMouseEvent) + """ + if WebBrowserWindow.autoScroller().mousePress(self, evt): + evt.accept() + return + + self.__mw.setEventMouseButtons(evt.buttons()) + self.__mw.setEventKeyboardModifiers(evt.modifiers()) + + if evt.button() == Qt.MouseButton.XButton1: + self.pageAction(QWebEnginePage.WebAction.Back).trigger() + evt.accept() + elif evt.button() == Qt.MouseButton.XButton2: + self.pageAction(QWebEnginePage.WebAction.Forward).trigger() + evt.accept() + + def _mouseReleaseEvent(self, evt): + """ + Protected method called by a mouse release event. + + @param evt reference to the mouse event (QMouseEvent) + """ + if WebBrowserWindow.autoScroller().mouseRelease(evt): + evt.accept() + return + + accepted = evt.isAccepted() + self.__page.event(evt) + if ( + not evt.isAccepted() and + self.__mw.eventMouseButtons() & Qt.MouseButton.MidButton + ): + url = QUrl(QApplication.clipboard().text( + QClipboard.Mode.Selection)) + if ( + not url.isEmpty() and + url.isValid() and + url.scheme() != "" + ): + self.__mw.setEventMouseButtons(Qt.MouseButton.NoButton) + self.__mw.setEventKeyboardModifiers( + Qt.KeyboardModifier.NoModifier) + self.setSource(url) + evt.setAccepted(accepted) + + def _mouseMoveEvent(self, evt): + """ + Protected method to handle mouse move events. + + @param evt reference to the mouse event (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 (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 (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 (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 (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 (QEvent) + @return flag indicating, if the event was handled (boolean) + """ + if evt.type() == QEvent.Type.Gesture: + self._gestureEvent(evt) + return True + + return super().event(evt) + + 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 (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 + """ + self.__siteIcon = icon + + from .Tools import WebIconProvider + WebIconProvider.instance().saveIcon(self) + + self.faviconChanged.emit() + + def icon(self): + """ + Public method to get the web site icon. + + @return web site icon + @rtype QIcon + """ + if not self.__siteIcon.isNull(): + return QIcon(self.__siteIcon) + + from .Tools import WebIconProvider + 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 (string) + """ + 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 + @type int + """ + if ( + status == + QWebEnginePage.RenderProcessTerminationStatus + .NormalTerminationStatus + ): + return + + QTimer.singleShot(0, lambda: 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( + e5App().style().standardIcon( + QStyle.StandardPixmap.SP_MessageBoxWarning).pixmap(48, 48) + ).toString()) + html = html.replace("@FAVICON@", pixmapToDataUrl( + e5App().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 (integer) + """ + self.__progress = progress + + def __loadFinished(self, ok): + """ + Private method to handle the loadFinished signal. + + @param ok flag indicating the result (boolean) + """ + self.__isLoading = False + self.__progress = 0 + + QApplication.processEvents() + QTimer.singleShot(200, self.__renderPreview) + + from .ZoomManager import ZoomManager + 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 + """ + modified = QDateTime.fromString(lastModified, "MM/dd/yyyy hh:mm:ss") + if modified.isValid(): + from WebBrowser.WebBrowserWindow import WebBrowserWindow + from .Bookmarks.BookmarkNode import BookmarkNode + manager = WebBrowserWindow.bookmarksManager() + for bookmark in manager.bookmarksForUrl(self.url()): + manager.setTimestamp(bookmark, BookmarkNode.TsModified, + modified) + + def isLoading(self): + """ + Public method to get the loading state. + + @return flag indicating the loading state (boolean) + """ + return self.__isLoading + + def progress(self): + """ + Public method to get the load progress. + + @return load progress (integer) + """ + 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, QWebEngineDownloadItem.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 Utilities.isWindowsPlatform(): + defaultFileName += ".mht" + else: + defaultFileName += ".mhtml" + + fileName = "" + saveFormat = QWebEngineDownloadItem.SavePageFormat.MimeHtmlSaveFormat + + fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + None, + self.tr("Save Web Page"), + defaultFileName, + ";;".join(filterList), + None) + if fileName: + index = filterList.index(selectedFilter) + if index == 0: + saveFormat = ( + QWebEngineDownloadItem.SavePageFormat.MimeHtmlSaveFormat + ) + elif index == 1: + saveFormat = ( + QWebEngineDownloadItem.SavePageFormat.SingleHtmlSaveFormat + ) + else: + saveFormat = ( + QWebEngineDownloadItem.SavePageFormat + .CompleteHtmlSaveFormat + ) + + extension = os.path.splitext(fileName)[1] + if not extension: + # add the platform specific default extension + if Utilities.isWindowsPlatform(): + extensionsIndex = 1 + else: + extensionsIndex = 0 + extensions = extensionsList[index] + fileName += extensions[extensionsIndex] + + return fileName, saveFormat + + ########################################################################### + ## Miscellaneous methods below + ########################################################################### + + def createWindow(self, windowType): + """ + Public method called, when a new window should be created. + + @param windowType type of the requested window + (QWebEnginePage.WebWindowType) + @return reference to the created browser window (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() + else: + return self.__mw.newTab(addNextTo=self, background=True) + + 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 (boolean) + """ + 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 (list of tuples of two strings) + """ + 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 (boolean) + """ + 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.SafeJsWorld) + + def __configureSpeedDial(self): + """ + Private slot to configure the speed dial. + """ + self.page().runJavaScript("configureSpeedDial();", + WebBrowserPage.SafeJsWorld) + + def __reloadAllSpeedDials(self): + """ + Private slot to reload all speed dials. + """ + self.page().runJavaScript("reloadAll();", WebBrowserPage.SafeJsWorld) + + 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) + """ + 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: + from .Tools import WebIconProvider + 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 from the page + ########################################################################### + + @pyqtSlot() + def __printPage(self): + """ + Private slot to support printing from the web page. + """ + self.__mw.tabWidget.printBrowser(browser=self) + + ########################################################################### + ## Methods below implement slots for Qt 5.11+ + ########################################################################### + + @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 + """ + 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 + from .Download.DownloadUtilities import dataString + sizeStr = dataString(quotaRequest.requestedSize()) + + ok = E5MessageBox.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 5.12+ + ########################################################################### + + @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 + """ + certificates = clientCertificateSelection.certificates() + if len(certificates) == 0: + clientCertificateSelection.selectNone() + elif len(certificates) == 1: + clientCertificateSelection.select(certificates[0]) + else: + certificate = None + from E5Network.E5SslCertificateSelectionDialog import ( + E5SslCertificateSelectionDialog + ) + dlg = E5SslCertificateSelectionDialog(certificates, self) + if dlg.exec() == QDialog.DialogCode.Accepted: + certificate = dlg.getSelectedCertificate() + + if certificate is None: + clientCertificateSelection.selectNone() + else: + clientCertificateSelection.select(certificate)