Continued porting the web browser. QtWebEngine

Mon, 08 Feb 2016 20:56:16 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 08 Feb 2016 20:56:16 +0100
branch
QtWebEngine
changeset 4728
0367675d783d
parent 4727
62b50a24fb59
child 4729
fc8d00860717

Continued porting the web browser.

- added web hit test functionality

WebBrowser/Tools/Scripts.py file | annotate | diff | comparison | revisions
WebBrowser/Tools/WebHitTestResult.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserPage.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserView.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserWindow.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
eric6_browser.py file | annotate | diff | comparison | revisions
--- a/WebBrowser/Tools/Scripts.py	Sun Feb 07 19:36:07 2016 +0100
+++ b/WebBrowser/Tools/Scripts.py	Mon Feb 08 20:56:16 2016 +0100
@@ -7,6 +7,11 @@
 Module containing function to generate JavaScript code.
 """
 
+#
+# This code was ported from QupZilla.
+# Copyright (C) David Rosca <nowrep@gmail.com>
+#
+
 from __future__ import unicode_literals
 
 from .WebBrowserTools import readAllFileContents
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/Tools/WebHitTestResult.py	Mon Feb 08 20:56:16 2016 +0100
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an object for testing certain aspects of a web page.
+"""
+
+#
+# This code was ported from QupZilla.
+# Copyright (C) David Rosca <nowrep@gmail.com>
+#
+
+from __future__ import unicode_literals
+
+from PyQt5.QtCore import QPoint, QRect, QUrl
+
+class WebHitTestResult(object):
+    """
+    Class implementing an object for testing certain aspects of a web page.
+    """
+    def __init__(self, page, pos):
+        """
+        Constructor
+        
+        @param page reference to the web page
+        @type WebBrowserPage
+        @param pos position to be tested
+        @type QPoint
+        """
+        self.__isNull = True
+        self.__isContentEditable = False
+        self.__isContentSelected = False
+        self.__isMediaPaused = False
+        self.__isMediaMuted = False
+        self.__pos = QPoint(pos)
+        self.__alternateText = ""
+        self.__boundingRect = QRect()
+        self.__imageUrl = QUrl()
+        self.__linkTitle = ""
+        self.__linkUrl = QUrl()
+        self.__mediaUrl = QUrl()
+        self.__tagName = ""
+    
+        script = """
+            (function() {{
+                var e = document.elementFromPoint({0}, {1});
+                if (!e)
+                    return;
+                function isMediaElement(e) {{
+                    return e.tagName == 'AUDIO' || e.tagName == 'VIDEO';
+                }}
+                function isEditableElement(e) {{
+                    if (e.isContentEditable)
+                        return true;
+                    if (e.tagName == 'INPUT' || e.tagName == 'TEXTAREA')
+                        return e.getAttribute('readonly') != 'readonly';
+                    return false;
+                }}
+                function isSelected(e) {{
+                    var selection = window.getSelection();
+                    if (selection.type != 'Range')
+                        return false;
+                    return window.getSelection().containsNode(e, true);
+                }}
+                var res = {{
+                    alternateText: e.getAttribute('alt'),
+                    boundingRect: '',
+                    imageUrl: '',
+                    contentEditable: isEditableElement(e),
+                    contentSelected: isSelected(e),
+                    linkTitle: '',
+                    linkUrl: '',
+                    mediaUrl: '',
+                    mediaPaused: false,
+                    mediaMuted: false,
+                    tagName: e.tagName.toLowerCase()
+                }};
+                var r = e.getBoundingClientRect();
+                res.boundingRect = [r.top, r.left, r.width, r.height];
+                if (e.tagName == 'IMG')
+                    res.imageUrl = e.getAttribute('src');
+                if (e.tagName == 'A') {{
+                    res.linkTitle = e.text;
+                    res.linkUrl = e.getAttribute('href');
+                }}
+                while (e) {{
+                    if (res.linkTitle == '' && e.tagName == 'A')
+                        res.linkTitle = e.text;
+                    if (res.linkUrl == '' && e.tagName == 'A')
+                        res.linkUrl = e.getAttribute('href');
+                    if (res.mediaUrl == '' && isMediaElement(e)) {{
+                        res.mediaUrl = e.currentSrc;
+                        res.mediaPaused = e.paused;
+                        res.mediaMuted = e.muted;
+                    }}
+                    e = e.parentElement;
+                }}
+                return res;
+            }})()
+        """.format(pos.x(), pos.y())
+        self.__populate(page.url(), page.execJavaScript(script))
+    
+    def alternateText(self):
+        """
+        Public method to get the alternate text.
+        
+        @return alternate text
+        @rtype str
+        """
+        return self.__alternateText
+    
+    def boundingRect(self):
+        """
+        Public method to get the bounding rectangle.
+        
+        @return bounding rectangle
+        @rtype QRect
+        """
+        return QRect(self.__boundingRect)
+    
+    def imageUrl(self):
+        """
+        Public method to get the URL of an image.
+        
+        @return image URL
+        @rtype QUrl
+        """
+        return self.__imageUrl
+    
+    def isContentEditable(self):
+        """
+        Public method to check for editable content.
+        
+        @return flag indicating editable content
+        @rtype bool
+        """
+        return self.__isContentEditable
+    
+    def isContentSelected(self):
+        """
+        Public method to check for selected content.
+        
+        @return flag indicating selected content
+        @rtype bool
+        """
+        return self.__isContentSelected
+    
+    def isNull(self):
+        """
+        Public method to test, if the hit test is empty.
+        
+        @return flag indicating an empty object
+        @rtype bool
+        """
+        return self.__isNull
+    
+    def linkTitle(self):
+        """
+        Public method to get the title for a link element.
+        
+        @return title for a link element
+        @rtype str
+        """
+        return self.__linkTitle
+    
+    def linkUrl(self):
+        """
+        Public method to get the URL for a link element.
+        
+        @return URL for a link element
+        @rtype QUrl
+        """
+        return self.__linkUrl
+    
+    def mediaUrl(self):
+        """
+        Public method to get the URL for a media element.
+        
+        @return URL for a media element
+        @rtype QUrl
+        """
+        return self.__mediaUrl
+    
+    def mediaPaused(self):
+        """
+        Public method to check, if a media element is paused.
+        
+        @return flag indicating a paused media element
+        @rtype bool
+        """
+        return self.__isMediaPaused
+    
+    def mediaMuted(self):
+        """
+        Public method to check, if a media element is muted.
+        
+        @return flag indicating a muted media element
+        @rtype bool
+        """
+        return self.__isMediaMuted
+    
+    def pos(self):
+        """
+        Public method to get the position of the hit test.
+        
+        @return position of hit test
+        @rtype QPoint
+        """
+        return QPoint(self.__pos)
+    
+    def tagName(self):
+        """
+        Public method to get the name of the tested tag.
+        
+        @return name of the tested tag
+        @rtype str
+        """
+        return self.__tagName
+    
+    def __populate(self, url, res):
+        """
+        Private method to populate the object.
+        
+        @param url URL of the tested page
+        @type QUrl
+        @param res dictionary with result data from JavaScript
+        @type dict
+        """
+        if not res:
+            return
+        
+        self.__alternateText = res["alternateText"]
+        self.__imageUrl = QUrl(res["imageUrl"])
+        self.__isContentEditable = res["contentEditable"]
+        self.__isContentSelected = res["contentSelected"]
+        self.__linkTitle = res["linkTitle"]
+        self.__linkUrl = QUrl(res["linkUrl"])
+        self.__mediaUrl = QUrl(res["mediaUrl"])
+        self.__isMediaPaused = res["mediaPaused"]
+        self.__isMediaMuted = res["mediaMuted"]
+        self.__tagName = res["tagName"]
+        
+        rect = res["boundingRect"]
+        if len(rect) == 4:
+            self.__boundingRect = QRect(int(rect[0]), int(rect[1]),
+                                        int(rect[2]), int(rect[3]))
+        
+        if not self.__imageUrl.isEmpty():
+            self.__imageUrl = url.resolved(self.__imageUrl)
+        if not self.__linkUrl.isEmpty():
+            self.__linkUrl = url.resolved(self.__linkUrl)
+        if not self.__mediaUrl.isEmpty():
+            self.__mediaUrl = url.resolved(self.__mediaUrl)
--- a/WebBrowser/WebBrowserPage.py	Sun Feb 07 19:36:07 2016 +0100
+++ b/WebBrowser/WebBrowserPage.py	Mon Feb 08 20:56:16 2016 +0100
@@ -34,6 +34,8 @@
 
 from .JavaScript.ExternalJsObject import ExternalJsObject
 
+from .Tools.WebHitTestResult import WebHitTestResult
+
 import Preferences
 import UI.PixmapCache
 import Globals
@@ -721,14 +723,14 @@
         """
         loop = QEventLoop()
         resultDict = {"res": None}
+        QTimer.singleShot(500, loop.quit);
         
         def resultCallback(res, resDict=resultDict):
             if loop and loop.isRunning():
                 resDict["res"] = res
                 loop.quit()
         
-        self.previewView.page().runJavaScript(
-            script, resultCallback)
+        self.runJavaScript(script, resultCallback)
         
         loop.exec_()
         return resultDict["res"]
@@ -749,15 +751,14 @@
     
     def hitTestContent(self, pos):
         """
-        Public method to test the contents at a given position.
+        Public method to test the content at a specified position.
         
-        @param pos position to be tested
+        @param pos position to execute the test at
         @type QPoint
-        @return object containing the test results
-        @rtype WebBrowserHitTestResult
+        @return test result object
+        @rtype WebHitTestResult
         """
-        # TODO: WebBrowserHitTestResult
-##        return WebBrowserHitTestResult(self, pos) 
+        return WebHitTestResult(self, pos)
     
     def setupWebChannel(self):
         """
--- a/WebBrowser/WebBrowserView.py	Sun Feb 07 19:36:07 2016 +0100
+++ b/WebBrowser/WebBrowserView.py	Mon Feb 08 20:56:16 2016 +0100
@@ -18,7 +18,7 @@
     QUrl, QBuffer, QIODevice, QFileInfo, Qt, QTimer, QEvent, \
     QRect, QFile, QPoint, QByteArray, qVersion
 from PyQt5.QtGui import QDesktopServices, QClipboard, QMouseEvent, QColor, \
-    QPalette, QIcon
+    QPalette, QIcon, QContextMenuEvent
 from PyQt5.QtWidgets import qApp, QStyle, QMenu, QApplication, QInputDialog, \
     QLineEdit, QLabel, QToolTip, QFrame, QDialog
 from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
@@ -556,6 +556,50 @@
     
     def contextMenuEvent(self, evt):
         """
+        Public 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
+    
+    # TODO: re-arrange the menu creation stuff to better adjust to the current situation
+##void TabbedWebView::_contextMenuEvent(QContextMenuEvent *event)
+##{
+##    m_menu->clear();
+##
+##    const WebHitTestResult hitTest = page()->hitTestContent(event->pos());
+##
+##    createContextMenu(m_menu, hitTest);
+##
+##    if (!hitTest.isContentEditable() && !hitTest.isContentSelected() && m_window) {
+##        m_menu->addAction(m_window->adBlockIcon()->menuAction());
+##    }
+##
+##    m_menu->addSeparator();
+##    m_menu->addAction(tr("Inspect Element"), this, SLOT(inspectElement()));
+##
+##    if (!m_menu->isEmpty()) {
+##        // Prevent choosing first option with double rightclick
+##        const QPoint pos = event->globalPos();
+##        QPoint p(pos.x(), pos.y() + 1);
+##
+##        m_menu->popup(p);
+##        return;
+##    }
+##
+##    WebView::_contextMenuEvent(event);
+##}
+    def _contextMenuEvent(self, evt):
+        """
         Protected method called to create a context menu.
         
         This method is overridden from QWebEngineView.
@@ -563,6 +607,8 @@
         @param evt reference to the context menu event object
             (QContextMenuEvent)
         """
+        hitTest = self.page().hitTestContent(evt.pos())
+        
         # TODO: User Agent
 ##        from .UserAgent.UserAgentMenu import UserAgentMenu
         menu = QMenu(self)
--- a/WebBrowser/WebBrowserWindow.py	Sun Feb 07 19:36:07 2016 +0100
+++ b/WebBrowser/WebBrowserWindow.py	Mon Feb 08 20:56:16 2016 +0100
@@ -129,9 +129,9 @@
         
         self.__isPrivate = private
         
-##        self.__eventMouseButtons = Qt.NoButton
-##        self.__eventKeyboardModifiers = Qt.NoModifier
-##        
+        self.__eventMouseButtons = Qt.NoButton
+        self.__eventKeyboardModifiers = Qt.NoModifier
+        
         if self.__initShortcutsOnly:
             self.__initActions()
         else:
--- a/eric6.e4p	Sun Feb 07 19:36:07 2016 +0100
+++ b/eric6.e4p	Mon Feb 08 20:56:16 2016 +0100
@@ -26,54 +26,6 @@
     <Source>DataViews/PyCoverageDialog.py</Source>
     <Source>DataViews/PyProfileDialog.py</Source>
     <Source>DataViews/__init__.py</Source>
-    <Source>DebugClients/Python/AsyncFile.py</Source>
-    <Source>DebugClients/Python/AsyncIO.py</Source>
-    <Source>DebugClients/Python/DCTestResult.py</Source>
-    <Source>DebugClients/Python/DebugBase.py</Source>
-    <Source>DebugClients/Python/DebugClient.py</Source>
-    <Source>DebugClients/Python/DebugClientBase.py</Source>
-    <Source>DebugClients/Python/DebugClientCapabilities.py</Source>
-    <Source>DebugClients/Python/DebugClientThreads.py</Source>
-    <Source>DebugClients/Python/DebugConfig.py</Source>
-    <Source>DebugClients/Python/DebugProtocol.py</Source>
-    <Source>DebugClients/Python/DebugThread.py</Source>
-    <Source>DebugClients/Python/FlexCompleter.py</Source>
-    <Source>DebugClients/Python/PyProfile.py</Source>
-    <Source>DebugClients/Python/__init__.py</Source>
-    <Source>DebugClients/Python/coverage/__init__.py</Source>
-    <Source>DebugClients/Python/coverage/__main__.py</Source>
-    <Source>DebugClients/Python/coverage/annotate.py</Source>
-    <Source>DebugClients/Python/coverage/backunittest.py</Source>
-    <Source>DebugClients/Python/coverage/backward.py</Source>
-    <Source>DebugClients/Python/coverage/bytecode.py</Source>
-    <Source>DebugClients/Python/coverage/cmdline.py</Source>
-    <Source>DebugClients/Python/coverage/collector.py</Source>
-    <Source>DebugClients/Python/coverage/config.py</Source>
-    <Source>DebugClients/Python/coverage/control.py</Source>
-    <Source>DebugClients/Python/coverage/data.py</Source>
-    <Source>DebugClients/Python/coverage/debug.py</Source>
-    <Source>DebugClients/Python/coverage/env.py</Source>
-    <Source>DebugClients/Python/coverage/execfile.py</Source>
-    <Source>DebugClients/Python/coverage/files.py</Source>
-    <Source>DebugClients/Python/coverage/html.py</Source>
-    <Source>DebugClients/Python/coverage/misc.py</Source>
-    <Source>DebugClients/Python/coverage/monkey.py</Source>
-    <Source>DebugClients/Python/coverage/parser.py</Source>
-    <Source>DebugClients/Python/coverage/phystokens.py</Source>
-    <Source>DebugClients/Python/coverage/pickle2json.py</Source>
-    <Source>DebugClients/Python/coverage/plugin.py</Source>
-    <Source>DebugClients/Python/coverage/plugin_support.py</Source>
-    <Source>DebugClients/Python/coverage/python.py</Source>
-    <Source>DebugClients/Python/coverage/pytracer.py</Source>
-    <Source>DebugClients/Python/coverage/report.py</Source>
-    <Source>DebugClients/Python/coverage/results.py</Source>
-    <Source>DebugClients/Python/coverage/summary.py</Source>
-    <Source>DebugClients/Python/coverage/templite.py</Source>
-    <Source>DebugClients/Python/coverage/test_helpers.py</Source>
-    <Source>DebugClients/Python/coverage/version.py</Source>
-    <Source>DebugClients/Python/coverage/xmlreport.py</Source>
-    <Source>DebugClients/Python/eric6dbgstub.py</Source>
-    <Source>DebugClients/Python/getpass.py</Source>
     <Source>DebugClients/Python3/AsyncFile.py</Source>
     <Source>DebugClients/Python3/AsyncIO.py</Source>
     <Source>DebugClients/Python3/DCTestResult.py</Source>
@@ -123,6 +75,54 @@
     <Source>DebugClients/Python3/coverage/xmlreport.py</Source>
     <Source>DebugClients/Python3/eric6dbgstub.py</Source>
     <Source>DebugClients/Python3/getpass.py</Source>
+    <Source>DebugClients/Python/AsyncFile.py</Source>
+    <Source>DebugClients/Python/AsyncIO.py</Source>
+    <Source>DebugClients/Python/DCTestResult.py</Source>
+    <Source>DebugClients/Python/DebugBase.py</Source>
+    <Source>DebugClients/Python/DebugClient.py</Source>
+    <Source>DebugClients/Python/DebugClientBase.py</Source>
+    <Source>DebugClients/Python/DebugClientCapabilities.py</Source>
+    <Source>DebugClients/Python/DebugClientThreads.py</Source>
+    <Source>DebugClients/Python/DebugConfig.py</Source>
+    <Source>DebugClients/Python/DebugProtocol.py</Source>
+    <Source>DebugClients/Python/DebugThread.py</Source>
+    <Source>DebugClients/Python/FlexCompleter.py</Source>
+    <Source>DebugClients/Python/PyProfile.py</Source>
+    <Source>DebugClients/Python/__init__.py</Source>
+    <Source>DebugClients/Python/coverage/__init__.py</Source>
+    <Source>DebugClients/Python/coverage/__main__.py</Source>
+    <Source>DebugClients/Python/coverage/annotate.py</Source>
+    <Source>DebugClients/Python/coverage/backunittest.py</Source>
+    <Source>DebugClients/Python/coverage/backward.py</Source>
+    <Source>DebugClients/Python/coverage/bytecode.py</Source>
+    <Source>DebugClients/Python/coverage/cmdline.py</Source>
+    <Source>DebugClients/Python/coverage/collector.py</Source>
+    <Source>DebugClients/Python/coverage/config.py</Source>
+    <Source>DebugClients/Python/coverage/control.py</Source>
+    <Source>DebugClients/Python/coverage/data.py</Source>
+    <Source>DebugClients/Python/coverage/debug.py</Source>
+    <Source>DebugClients/Python/coverage/env.py</Source>
+    <Source>DebugClients/Python/coverage/execfile.py</Source>
+    <Source>DebugClients/Python/coverage/files.py</Source>
+    <Source>DebugClients/Python/coverage/html.py</Source>
+    <Source>DebugClients/Python/coverage/misc.py</Source>
+    <Source>DebugClients/Python/coverage/monkey.py</Source>
+    <Source>DebugClients/Python/coverage/parser.py</Source>
+    <Source>DebugClients/Python/coverage/phystokens.py</Source>
+    <Source>DebugClients/Python/coverage/pickle2json.py</Source>
+    <Source>DebugClients/Python/coverage/plugin.py</Source>
+    <Source>DebugClients/Python/coverage/plugin_support.py</Source>
+    <Source>DebugClients/Python/coverage/python.py</Source>
+    <Source>DebugClients/Python/coverage/pytracer.py</Source>
+    <Source>DebugClients/Python/coverage/report.py</Source>
+    <Source>DebugClients/Python/coverage/results.py</Source>
+    <Source>DebugClients/Python/coverage/summary.py</Source>
+    <Source>DebugClients/Python/coverage/templite.py</Source>
+    <Source>DebugClients/Python/coverage/test_helpers.py</Source>
+    <Source>DebugClients/Python/coverage/version.py</Source>
+    <Source>DebugClients/Python/coverage/xmlreport.py</Source>
+    <Source>DebugClients/Python/eric6dbgstub.py</Source>
+    <Source>DebugClients/Python/getpass.py</Source>
     <Source>DebugClients/__init__.py</Source>
     <Source>Debugger/BreakPointModel.py</Source>
     <Source>Debugger/BreakPointViewer.py</Source>
@@ -1273,6 +1273,7 @@
     <Source>WebBrowser/SearchWidget.py</Source>
     <Source>WebBrowser/Tools/Scripts.py</Source>
     <Source>WebBrowser/Tools/WebBrowserTools.py</Source>
+    <Source>WebBrowser/Tools/WebHitTestResult.py</Source>
     <Source>WebBrowser/Tools/WebIconLoader.py</Source>
     <Source>WebBrowser/Tools/WebIconProvider.py</Source>
     <Source>WebBrowser/Tools/__init__.py</Source>
@@ -1726,14 +1727,14 @@
   <Interfaces/>
   <Others>
     <Other>.hgignore</Other>
-    <Other>APIs/Python/zope-2.10.7.api</Other>
-    <Other>APIs/Python/zope-2.11.2.api</Other>
-    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/Python3/PyQt4.bas</Other>
     <Other>APIs/Python3/PyQt5.bas</Other>
     <Other>APIs/Python3/QScintilla2.bas</Other>
     <Other>APIs/Python3/eric6.api</Other>
     <Other>APIs/Python3/eric6.bas</Other>
+    <Other>APIs/Python/zope-2.10.7.api</Other>
+    <Other>APIs/Python/zope-2.11.2.api</Other>
+    <Other>APIs/Python/zope-3.3.1.api</Other>
     <Other>APIs/QSS/qss.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.api</Other>
     <Other>APIs/Ruby/Ruby-1.8.7.bas</Other>
@@ -1742,8 +1743,8 @@
     <Other>CSSs</Other>
     <Other>CodeTemplates</Other>
     <Other>DTDs</Other>
+    <Other>DebugClients/Python3/coverage/doc</Other>
     <Other>DebugClients/Python/coverage/doc</Other>
-    <Other>DebugClients/Python3/coverage/doc</Other>
     <Other>DesignerTemplates</Other>
     <Other>Dictionaries</Other>
     <Other>Documentation/Help</Other>
--- a/eric6_browser.py	Sun Feb 07 19:36:07 2016 +0100
+++ b/eric6_browser.py	Mon Feb 08 20:56:16 2016 +0100
@@ -30,7 +30,7 @@
 import sys
 import os
 
-# TODO: adjust this when done
+# TODO: adjust this to 5.6.0 when done
 MIN_QT_VERSION = "5.5.0"
 
 from PyQt5.QtCore import qVersion

eric ide

mercurial