From 86854dad53d13112b45db7ee1fde9c84378df03f Mon Sep 17 00:00:00 2001 From: Andy Stewart Date: Wed, 18 Jul 2018 20:59:15 +0800 Subject: [PATCH] Port application code from QtWebKit to QtWebEngine. --- README.md | 15 +-- app/browser/buffer.py | 212 +---------------------------- app/markdownpreviewer/buffer.py | 45 +------ app/orgpreviewer/buffer.py | 49 +------ core/browser.py | 229 ++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 311 deletions(-) create mode 100644 core/browser.py diff --git a/README.md b/README.md index fe212d0..ad0e7bb 100644 --- a/README.md +++ b/README.md @@ -37,21 +37,12 @@ Using this framework, you can use PyQt develop powerful graphics programs to ext ## Installation -1. Install python libraries: +1. Install python dependences: -### ArchLinux +Make sure python3 and pip3 has install in your operating system, then execute below command: ```Bash -sudo pacman -S python-xlib python-pyqt5 -sudo pip install dbus-python PyMuPDF grip qrcode -``` - -### Debian - -```Bash -sudo apt-get update -sudo apt-get install python3-xlib python3-pyqt5 -sudo pip3 install dbus-python PyMuPDF grip qrcode +sudo pip3 install dbus-python PyMuPDF grip qrcode pyqt5 python-xlib ``` ### Package description. diff --git a/app/browser/buffer.py b/app/browser/buffer.py index dde45dc..170171c 100644 --- a/app/browser/buffer.py +++ b/app/browser/buffer.py @@ -19,13 +19,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from PyQt5 import QtWebEngineWidgets -from PyQt5 import QtCore -from PyQt5.QtCore import Qt, QEvent, QPointF, QEventLoop, QVariant, QTimer -from PyQt5.QtCore import QUrl, Qt -from PyQt5.QtGui import QColor, QDesktopServices -from PyQt5.QtWidgets import QApplication, QWidget - +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QColor +from core.browser import BrowserView from core.buffer import Buffer class AppBuffer(Buffer): @@ -41,205 +37,3 @@ class AppBuffer(Buffer): # We need send key event to QWebEngineView's child, not QWebEngineView. for child in self.buffer_widget.children(): QApplication.sendEvent(child, event) - -class BrowserView(QtWebEngineWidgets.QWebEngineView): - - open_url_in_new_tab = QtCore.pyqtSignal(str) - - def __init__(self): - super(QtWebEngineWidgets.QWebEngineView, self).__init__() - - self.installEventFilter(self) - - self.web_page = BrowserPage() - self.setPage(self.web_page) - - def event(self, event): - if event.type() == QEvent.ChildAdded: - obj = event.child() - if isinstance(obj, QWidget): - obj.installEventFilter(self) - - return QtWebEngineWidgets.QWebEngineView.event(self, event) - - def eventFilter(self, obj, event): - if event.type() == QEvent.MouseButtonRelease: - hit = self.web_page.hitTestContent(event.pos()) - clicked_url = hit.linkUrl() - base_url = hit.baseUrl() - - if clicked_url != base_url and clicked_url != '': - result = "" - if 'http://' in clicked_url or 'https://' in clicked_url: - result = clicked_url - elif clicked_url == "#": - result = base_url + clicked_url - else: - result = "http://" + base_url.split("/")[2] + clicked_url - - modifiers = QApplication.keyboardModifiers() - - if modifiers != Qt.ControlModifier: - # Load url in current tab. - self.setUrl(QUrl(result)) - else: - # Load url in new tab if user press ctrl modifier. - self.open_url_in_new_tab.emit(result) - - return True - - event.accept() - return True - - return super(QtWebEngineWidgets.QWebEngineView, self).eventFilter(obj, event) - -class BrowserPage(QtWebEngineWidgets.QWebEnginePage): - - def __init__(self): - QtWebEngineWidgets.QWebEnginePage.__init__(self) - - def hitTestContent(self, pos): - return WebHitTestResult(self, pos) - - def mapToViewport(self, pos): - return QPointF(pos.x(), pos.y()) - - def executeJavaScript(self, scriptSrc): - self.loop = QEventLoop() - self.result = QVariant() - QTimer.singleShot(250, self.loop.quit) - - self.runJavaScript(scriptSrc, self.callbackJS) - self.loop.exec_() - self.loop = None - return self.result - - def callbackJS(self, res): - if self.loop is not None and self.loop.isRunning(): - self.result = res - self.loop.quit() - -class WebHitTestResult(): - def __init__(self, page, pos): - self.page = page - self.pos = pos - self.m_linkUrl = self.page.url().toString() - self.m_baseUrl = self.page.url().toString() - self.viewportPos = self.page.mapToViewport(self.pos) - self.source = """(function() { - let e = document.elementFromPoint(%1, %2); - 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) { - let selection = window.getSelection(); - if (selection.type !== 'Range') - return false; - return window.getSelection().containsNode(e, true); - }; - let res = { - baseUrl: document.baseURI, - alternateText: e.getAttribute('alt'), - boundingRect: '', - imageUrl: '', - contentEditable: isEditableElement(e), - contentSelected: isSelected(e), - linkTitle: '', - linkUrl: '', - mediaUrl: '', - tagName: e.tagName.toLowerCase() - }; - let 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 === '') { - 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; - })()""" - - self.js = self.source.replace("%1", str(self.viewportPos.x())).replace("%2", str(self.viewportPos.y())) - self.dic = self.page.executeJavaScript(self.js) - if self.dic is None: - return - - self.m_isNull = False - self.m_baseUrl = self.dic["baseUrl"] - self.m_alternateText = self.dic["alternateText"] - self.m_imageUrl = self.dic["imageUrl"] - self.m_isContentEditable = self.dic["contentEditable"] - self.m_isContentSelected = self.dic["contentSelected"] - self.m_linkTitle = self.dic["linkTitle"] - self.m_linkUrl = self.dic["linkUrl"] - self.m_mediaUrl = self.dic["mediaUrl"] - try: - self.m_mediaPaused = self.dic["mediaPaused"] - self.m_mediaMuted = self.dic["mediaMuted"] - except: - pass - self.m_tagName = self.dic["tagName"] - - def linkUrl(self): - return self.m_linkUrl - - def isContentEditable(self): - return self.m_isContentEditable - - def isContentSelected(self): - return self.m_isContentSelected - - def imageUrl(self): - try: - return self.m_imageUrl - except: - return "" - - def mediaUrl(self): - return self.m_mediaUrl - - def baseUrl(self): - return self.m_baseUrl - - def updateWithContextMenuData(self, data): - if data.isValid(): - pass - else: - return - - self.m_linkTitle = data.linkText() - self.m_linkUrl = data.linkUrl().toString() - self.m_isContentEditable = data.isContentEditable() - if data.selectedText() == "": - self.m_isContentSelected = False - else: - self.m_isContentSelected = True - - if data.mediaType() == QWebEngineContextMenuData.MediaTypeImage: - self.m_imageUrl = data.mediaUrl().toString() - elif data.mediaType() == QWebEngineContextMenuData.MediaTypeAudio or data.mediaType() == QWebEngineContextMenuData.MediaTypeVideo: - self.m_mediaUrl = data.mediaUrl().toString() diff --git a/app/markdownpreviewer/buffer.py b/app/markdownpreviewer/buffer.py index 0f52a6e..25f78ab 100644 --- a/app/markdownpreviewer/buffer.py +++ b/app/markdownpreviewer/buffer.py @@ -19,12 +19,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from PyQt5 import QtCore from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor -from PyQt5.QtWebKitWidgets import QWebView, QWebPage -from PyQt5.QtWidgets import QApplication -from PyQt5.QtWebKit import QWebSettings +from core.browser import BrowserView from core.buffer import Buffer from core.utils import PostGui import socket @@ -44,7 +41,7 @@ class AppBuffer(Buffer): subprocess.Popen("grip {0} {1}".format(url, self.port), shell=True) # Init widget. - self.add_widget(BrowserWidget()) + self.add_widget(BrowserView()) # Add timer make load markdown preview link after grip process start finish. timer = threading.Timer(2, self.load_markdown_server) @@ -69,41 +66,3 @@ class AppBuffer(Buffer): paths = os.path.split(self.url) if len(paths) > 0: self.change_title(paths[-1]) - -class BrowserWidget(QWebView): - - def __init__(self): - super(QWebView, self).__init__() - - self.web_page = WebPage() - self.setPage(self.web_page) - - self.settings().setAttribute(QWebSettings.PluginsEnabled, True) - self.settings().setAttribute(QWebSettings.JavascriptEnabled, True) - self.settings().setAttribute(QWebSettings.JavascriptCanOpenWindows, True) - -class WebPage(QWebPage): - - open_url_in_new_tab = QtCore.pyqtSignal(str) - - def __init__(self): - super(WebPage, self).__init__() - - def acceptNavigationRequest(self, frame, request, type): - modifiers = QApplication.keyboardModifiers() - - # Handle myself if got user event. - if type == QWebPage.NavigationTypeLinkClicked: - if modifiers == Qt.ControlModifier: - self.open_url_in_new_tab.emit(request.url().toString()) - else: - self.view().load(request.url()) - - # Return False to stop default behavior. - return False - - # # Otherwise, use default behavior. - return QWebPage.acceptNavigationRequest(self, frame, request, type) - - def javaScriptConsoleMessage(self, msg, lineNumber, sourceID): - pass diff --git a/app/orgpreviewer/buffer.py b/app/orgpreviewer/buffer.py index a59463e..bf3f6d9 100644 --- a/app/orgpreviewer/buffer.py +++ b/app/orgpreviewer/buffer.py @@ -19,17 +19,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from PyQt5 import QtCore from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor -from PyQt5.QtWebKitWidgets import QWebView, QWebPage -from PyQt5.QtWidgets import QApplication -from PyQt5.QtWebKit import QWebSettings +from core.browser import BrowserView from core.buffer import Buffer -from core.utils import PostGui -import socket -import subprocess -import threading import os class AppBuffer(Buffer): @@ -37,7 +30,7 @@ class AppBuffer(Buffer): Buffer.__init__(self, buffer_id, url, False, QColor(255, 255, 255, 255)) self.url = url - self.add_widget(BrowserWidget()) + self.add_widget(BrowserView()) self.load_org_html_file() @@ -47,41 +40,3 @@ class AppBuffer(Buffer): def update_with_data(self, update_data): self.load_org_html_file() self.buffer_widget.reload() - -class BrowserWidget(QWebView): - - def __init__(self): - super(QWebView, self).__init__() - - self.web_page = WebPage() - self.setPage(self.web_page) - - self.settings().setAttribute(QWebSettings.PluginsEnabled, True) - self.settings().setAttribute(QWebSettings.JavascriptEnabled, True) - self.settings().setAttribute(QWebSettings.JavascriptCanOpenWindows, True) - -class WebPage(QWebPage): - - open_url_in_new_tab = QtCore.pyqtSignal(str) - - def __init__(self): - super(WebPage, self).__init__() - - def acceptNavigationRequest(self, frame, request, type): - modifiers = QApplication.keyboardModifiers() - - # Handle myself if got user event. - if type == QWebPage.NavigationTypeLinkClicked: - if modifiers == Qt.ControlModifier: - self.open_url_in_new_tab.emit(request.url().toString()) - else: - self.view().load(request.url()) - - # Return False to stop default behavior. - return False - - # # Otherwise, use default behavior. - return QWebPage.acceptNavigationRequest(self, frame, request, type) - - def javaScriptConsoleMessage(self, msg, lineNumber, sourceID): - pass diff --git a/core/browser.py b/core/browser.py new file mode 100644 index 0000000..b52c5ed --- /dev/null +++ b/core/browser.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (C) 2018 Andy Stewart +# +# Author: Andy Stewart +# Maintainer: Andy Stewart +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from PyQt5 import QtCore +from PyQt5 import QtWebEngineWidgets +from PyQt5.QtCore import QUrl, Qt +from PyQt5.QtCore import Qt, QEvent, QPointF, QEventLoop, QVariant, QTimer +from PyQt5.QtGui import QColor +from PyQt5.QtWidgets import QApplication, QWidget + +class BrowserView(QtWebEngineWidgets.QWebEngineView): + + open_url_in_new_tab = QtCore.pyqtSignal(str) + + def __init__(self): + super(QtWebEngineWidgets.QWebEngineView, self).__init__() + + self.installEventFilter(self) + + self.web_page = BrowserPage() + self.setPage(self.web_page) + + def event(self, event): + if event.type() == QEvent.ChildAdded: + obj = event.child() + if isinstance(obj, QWidget): + obj.installEventFilter(self) + + return QtWebEngineWidgets.QWebEngineView.event(self, event) + + def eventFilter(self, obj, event): + if event.type() == QEvent.MouseButtonRelease: + hit = self.web_page.hitTestContent(event.pos()) + clicked_url = hit.linkUrl() + base_url = hit.baseUrl() + + if clicked_url != base_url and clicked_url != '': + result = "" + if 'http://' in clicked_url or 'https://' in clicked_url: + result = clicked_url + elif clicked_url == "#": + result = base_url + clicked_url + else: + result = "http://" + base_url.split("/")[2] + clicked_url + + modifiers = QApplication.keyboardModifiers() + + if modifiers != Qt.ControlModifier: + # Load url in current tab. + self.setUrl(QUrl(result)) + else: + # Load url in new tab if user press ctrl modifier. + self.open_url_in_new_tab.emit(result) + + return True + + event.accept() + return True + + return super(QtWebEngineWidgets.QWebEngineView, self).eventFilter(obj, event) + +class BrowserPage(QtWebEngineWidgets.QWebEnginePage): + + def __init__(self): + QtWebEngineWidgets.QWebEnginePage.__init__(self) + + def hitTestContent(self, pos): + return WebHitTestResult(self, pos) + + def mapToViewport(self, pos): + return QPointF(pos.x(), pos.y()) + + def executeJavaScript(self, scriptSrc): + self.loop = QEventLoop() + self.result = QVariant() + QTimer.singleShot(250, self.loop.quit) + + self.runJavaScript(scriptSrc, self.callbackJS) + self.loop.exec_() + self.loop = None + return self.result + + def callbackJS(self, res): + if self.loop is not None and self.loop.isRunning(): + self.result = res + self.loop.quit() + +class WebHitTestResult(): + def __init__(self, page, pos): + self.page = page + self.pos = pos + self.m_linkUrl = self.page.url().toString() + self.m_baseUrl = self.page.url().toString() + self.viewportPos = self.page.mapToViewport(self.pos) + self.source = """(function() { + let e = document.elementFromPoint(%1, %2); + 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) { + let selection = window.getSelection(); + if (selection.type !== 'Range') + return false; + return window.getSelection().containsNode(e, true); + }; + let res = { + baseUrl: document.baseURI, + alternateText: e.getAttribute('alt'), + boundingRect: '', + imageUrl: '', + contentEditable: isEditableElement(e), + contentSelected: isSelected(e), + linkTitle: '', + linkUrl: '', + mediaUrl: '', + tagName: e.tagName.toLowerCase() + }; + let 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 === '') { + 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; + })()""" + + self.js = self.source.replace("%1", str(self.viewportPos.x())).replace("%2", str(self.viewportPos.y())) + self.dic = self.page.executeJavaScript(self.js) + if self.dic is None: + return + + self.m_isNull = False + self.m_baseUrl = self.dic["baseUrl"] + self.m_alternateText = self.dic["alternateText"] + self.m_imageUrl = self.dic["imageUrl"] + self.m_isContentEditable = self.dic["contentEditable"] + self.m_isContentSelected = self.dic["contentSelected"] + self.m_linkTitle = self.dic["linkTitle"] + self.m_linkUrl = self.dic["linkUrl"] + self.m_mediaUrl = self.dic["mediaUrl"] + try: + self.m_mediaPaused = self.dic["mediaPaused"] + self.m_mediaMuted = self.dic["mediaMuted"] + except: + pass + self.m_tagName = self.dic["tagName"] + + def linkUrl(self): + return self.m_linkUrl + + def isContentEditable(self): + return self.m_isContentEditable + + def isContentSelected(self): + return self.m_isContentSelected + + def imageUrl(self): + try: + return self.m_imageUrl + except: + return "" + + def mediaUrl(self): + return self.m_mediaUrl + + def baseUrl(self): + return self.m_baseUrl + + def updateWithContextMenuData(self, data): + if data.isValid(): + pass + else: + return + + self.m_linkTitle = data.linkText() + self.m_linkUrl = data.linkUrl().toString() + self.m_isContentEditable = data.isContentEditable() + if data.selectedText() == "": + self.m_isContentSelected = False + else: + self.m_isContentSelected = True + + if data.mediaType() == QWebEngineContextMenuData.MediaTypeImage: + self.m_imageUrl = data.mediaUrl().toString() + elif data.mediaType() == QWebEngineContextMenuData.MediaTypeAudio or data.mediaType() == QWebEngineContextMenuData.MediaTypeVideo: + self.m_mediaUrl = data.mediaUrl().toString()