Beispiel #1
0
    def _initPreviewToTextSync(self):
        """Initialize the system per items 1, 2, and 4 above."""
        # When a web page finishes loading, reinsert our JavaScript.
        page = self._dock._widget.webEngineView.page()

        # Insert our scripts into every loaded page.
        qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
        if not qwebchannel_js.open(QIODevice.ReadOnly):
            raise SystemExit(
                'Failed to load qwebchannel.js with error: %s' %
                qwebchannel_js.errorString())
        qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')

        # Set up the QWebChannel. See http://doc.qt.io/qt-5/qtwebchannel-javascript.html.
        # Run the script containing QWebChannel.js first.
        beforeScript = QWebEngineScript()
        beforeScript.setSourceCode(qwebchannel_js + self._jsPreviewSync + self._qtJsInit)
        beforeScript.setName('qwebchannel.js, previewSync')
        # Run this JavaScript separated from any JavaScript present in the loaded web page. This provides better security (rogue pages can't access the QWebChannel) and better isolation (handlers, etc. won't conflict, I hope).
        beforeScript.setWorldId(QWebEngineScript.ApplicationWorld)
        beforeScript.setInjectionPoint(QWebEngineScript.DocumentCreation)
        # Per `setWebChannel <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`_, only one channel is allowed per page. So, don't run this on sub-frames, since it will attempt the creation of more channels for each subframe.
        beforeScript.setRunsOnSubFrames(False)
        page.scripts().insert(beforeScript)

        # Set up the web channel. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html
        # and http://stackoverflow.com/questions/28565254/how-to-use-qt-webengine-and-qwebchannel.
        # For debug, ``set QTWEBENGINE_REMOTE_DEBUGGING=port`` then browse to
        # http://127.0.0.1:port, where port=60000 works for me. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html.
        self.channel = QWebChannel(page)
        self.channel.registerObject("previewSync", self)
        # Expose the ``qt.webChannelTransport`` object in the world where these scripts live.
        page.setWebChannel(self.channel, QWebEngineScript.ApplicationWorld)
Beispiel #2
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):

        super(MainWindow, self).__init__(parent)

        self.esaskyBrowserWidget = ESASkyWrapper(self)
        if showCommands:
            self.controlWidget = ControlBox(self.esaskyBrowserWidget)

        _hwidget = QWidget()
        _hlayout = QHBoxLayout()
        _hwidget.setLayout(_hlayout)

        self.tableWidget = ResultTable()
        _vlayout = QVBoxLayout()
        _vlayout.addWidget(_hwidget)
        _vlayout.addWidget(self.tableWidget)
        _vwidget = QWidget()
        _vwidget.setLayout(_vlayout)

        if showCommands:
            _hlayout.addWidget(self.controlWidget, 1)
            _hlayout.addWidget(self.esaskyBrowserWidget, 2)
        else:
            _hlayout.addWidget(self.esaskyBrowserWidget, 1)

        # setup channel
        self.channel = QWebChannel()
        self.channel.registerObject('backend', self)
        self.esaskyBrowserWidget.page().setWebChannel(self.channel)

        self.setCentralWidget(_vwidget)

        #QObject.connect(a,SIGNAL("testInit"),self.pippo,Qt.QueuedConnection)

    def pippo(self):
        return "test"

    def setResult(self, result):
        self.controlWidget.getCurrentWidget().prepareTabularOutput(result)

    @pyqtSlot(str)
    def foo(self, arg1):
        import json
        mydict = json.loads(arg1)

        #print (type(self.controlWidget.getCurrentWidget()))
        try:
            astroTable = self.controlWidget.getCurrentWidget(
            ).prepareTabularOutput(mydict)
            self.tableWidget.setContent(astroTable)
        except AttributeError:
            print('MainWindow->foo->attribute not found')

    def setFov(self, fov):
        self.esaskyBrowserWidget.page().runJavaScript("setFov(" + str(fov) +
                                                      ")")

    def initTest(self):
        print('sending initTest')
        self.esaskyBrowserWidget.page().runJavaScript("initTest()")
Beispiel #3
0
 def addobject(self, name):
     if app is not None:
         if self.channel is None:
             self.channel = QWebChannel()
         self.channel.registerObject(name, app.frontendpy)
         self.setWebChannel(self.channel)
Beispiel #4
0
        self.load(QUrl('file:///html/final.html'))
        self.resize(1366,768)
        self.show()
        # self.showFullScreen()
        t = threading.Thread(target=self.size_fix)
        t.setDaemon(True)
        t.start()
        
    def size_fix(self):
        while True:
            self.p.setZoomFactor(self.width()/1366)
            time.sleep(0.15)
pass
pass
pass
pass
'nya'
pass

if __name__=='__main__':
    app = QApplication([])
    
    channel = QWebChannel()
    handler = CallHandler()
    channel.registerObject('handler', handler)
    
    view = my_view()
    
    app.exec_()
    
    data.kiri_save()
Beispiel #5
0
class BrowserView(QMainWindow):
    instances = {}

    create_window_trigger = QtCore.pyqtSignal(object)
    set_title_trigger = QtCore.pyqtSignal(str)
    load_url_trigger = QtCore.pyqtSignal(str)
    html_trigger = QtCore.pyqtSignal(str, str)
    dialog_trigger = QtCore.pyqtSignal(int, str, bool, str, str)
    destroy_trigger = QtCore.pyqtSignal()
    fullscreen_trigger = QtCore.pyqtSignal()
    current_url_trigger = QtCore.pyqtSignal()
    evaluate_js_trigger = QtCore.pyqtSignal(str)

    class JSBridge(QtCore.QObject):
        api = None
        parent_uid = None

        try:
            qtype = QtCore.QJsonValue  # QT5
        except AttributeError:
            qtype = str  # QT4

        def __init__(self):
            super(BrowserView.JSBridge, self).__init__()

        @QtCore.pyqtSlot(str, qtype, result=str)
        def call(self, func_name, param):
            func_name = BrowserView._convert_string(func_name)
            param = BrowserView._convert_string(param)

            return _js_bridge_call(self.parent_uid, self.api, func_name, param)

    def __init__(self, uid, title, url, width, height, resizable, fullscreen,
                 min_size, confirm_quit, background_color, debug, js_api,
                 webview_ready):
        super(BrowserView, self).__init__()
        BrowserView.instances[uid] = self
        self.uid = uid

        self.is_fullscreen = False
        self.confirm_quit = confirm_quit

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore()
        self._evaluate_js_semaphore = Semaphore(0)
        self.load_event = Event()

        self._evaluate_js_result = None
        self._current_url = None
        self._file_name = None

        self.resize(width, height)
        self.title = title
        self.setWindowTitle(title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not resizable:
            self.setFixedSize(width, height)

        self.setMinimumSize(min_size[0], min_size[1])

        self.view = QWebView(self)

        if url is not None:
            self.view.setUrl(QtCore.QUrl(url))

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.api = js_api
        self.js_bridge.parent_uid = self.uid

        if _qt_version >= [5, 5]:
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        self.view.page().loadFinished.connect(self.on_load_finished)

        if fullscreen:
            self.toggle_fullscreen()

        self.view.setContextMenuPolicy(
            QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.move(QApplication.desktop().availableGeometry().center() -
                  self.rect().center())
        self.activateWindow()
        self.raise_()
        webview_ready.set()

    def on_set_title(self, title):
        self.setWindowTitle(title)

    def on_file_dialog(self, dialog_type, directory, allow_multiple,
                       save_filename, file_filter):
        if dialog_type == FOLDER_DIALOG:
            self._file_name = QFileDialog.getExistingDirectory(
                self,
                localization['linux.openFolder'],
                options=QFileDialog.ShowDirsOnly)
        elif dialog_type == OPEN_DIALOG:
            if allow_multiple:
                self._file_name = QFileDialog.getOpenFileNames(
                    self, localization['linux.openFiles'], directory,
                    file_filter)
            else:
                self._file_name = QFileDialog.getOpenFileName(
                    self, localization['linux.openFile'], directory,
                    file_filter)
        elif dialog_type == SAVE_DIALOG:
            if directory:
                save_filename = os.path.join(str(directory),
                                             str(save_filename))

            self._file_name = QFileDialog.getSaveFileName(
                self, localization['global.saveFile'], save_filename)

        self._file_name_semaphore.release()

    def on_current_url(self):
        self._current_url = self.view.url().toString()
        self._current_url_semaphore.release()

    def on_load_url(self, url):
        self.view.setUrl(QtCore.QUrl(url))

    def on_load_html(self, content, base_uri):
        self.view.setHtml(content, QtCore.QUrl(base_uri))

    def closeEvent(self, event):
        if self.confirm_quit:
            reply = QMessageBox.question(
                self, self.title, localization['global.quitConfirmation'],
                QMessageBox.Yes, QMessageBox.No)

            if reply == QMessageBox.No:
                event.ignore()
                return

        event.accept()
        del BrowserView.instances[self.uid]

    def on_destroy_window(self):
        self.close()

    def on_fullscreen(self):
        if self.is_fullscreen:
            self.showNormal()
        else:
            self.showFullScreen()

        self.is_fullscreen = not self.is_fullscreen

    def on_evaluate_js(self, script):
        def return_result(result):
            self._evaluate_js_result = result
            self._evaluate_js_semaphore.release()

        try:  # PyQt4
            return_result(
                self.view.page().mainFrame().evaluateJavaScript(script))
        except AttributeError:  # PyQt5
            self.view.page().runJavaScript(script, return_result)

    def on_load_finished(self):
        if self.js_bridge.api:
            self._set_js_api()
        else:
            self.load_event.set()

    def set_title(self, title):
        self.set_title_trigger.emit(title)

    def get_current_url(self):
        self.current_url_trigger.emit()
        self._current_url_semaphore.acquire()

        return self._current_url

    def load_url(self, url):
        self.load_event.clear()
        self.load_url_trigger.emit(url)

    def load_html(self, content, base_uri):
        self.load_event.clear()
        self.html_trigger.emit(content, base_uri)

    def create_file_dialog(self, dialog_type, directory, allow_multiple,
                           save_filename, file_filter):
        self.dialog_trigger.emit(dialog_type, directory, allow_multiple,
                                 save_filename, file_filter)
        self._file_name_semaphore.acquire()

        if _qt_version >= [5, 0]:  # QT5
            if dialog_type == FOLDER_DIALOG:
                file_names = (self._file_name, )
            elif dialog_type == SAVE_DIALOG or not allow_multiple:
                file_names = (self._file_name[0], )
            else:
                file_names = tuple(self._file_name[0])

        else:  # QT4
            if dialog_type == FOLDER_DIALOG:
                file_names = (BrowserView._convert_string(self._file_name), )
            elif dialog_type == SAVE_DIALOG or not allow_multiple:
                file_names = (BrowserView._convert_string(
                    self._file_name[0]), )
            else:
                file_names = tuple(
                    [BrowserView._convert_string(s) for s in self._file_name])

        # Check if we got an empty tuple, or a tuple with empty string
        if len(file_names) == 0 or len(file_names[0]) == 0:
            return None
        else:
            return file_names

    def destroy_(self):
        self.destroy_trigger.emit()

    def toggle_fullscreen(self):
        self.fullscreen_trigger.emit()

    def evaluate_js(self, script):
        self.load_event.wait()

        self.evaluate_js_trigger.emit(script)
        self._evaluate_js_semaphore.acquire()

        return self._evaluate_js_result

    def _set_js_api(self):
        def _register_window_object():
            frame.addToJavaScriptWindowObject('external', self.js_bridge)

        script = _parse_api_js(self.js_bridge.api)

        if _qt_version >= [5, 5]:
            qwebchannel_js = QtCore.QFile('://qtwebchannel/qwebchannel.js')
            if qwebchannel_js.open(QtCore.QFile.ReadOnly):
                source = bytes(qwebchannel_js.readAll()).decode('utf-8')
                self.view.page().runJavaScript(source)
                self.channel.registerObject('external', self.js_bridge)
                qwebchannel_js.close()
        elif _qt_version >= [5, 0]:
            frame = self.view.page().mainFrame()
            _register_window_object()
        else:
            frame = self.view.page().mainFrame()
            _register_window_object()

        try:  # PyQt4
            self.view.page().mainFrame().evaluateJavaScript(script)
        except AttributeError:  # PyQt5
            self.view.page().runJavaScript(script)

        self.load_event.set()

    @staticmethod
    def _convert_string(qstring):
        try:
            qstring = qstring.toString()  # QJsonValue conversion
        except:
            pass

        if sys.version < '3':
            return unicode(qstring)
        else:
            return str(qstring)

    @staticmethod
    # Receive func from subthread and execute it on the main thread
    def on_create_window(func):
        func()
Beispiel #6
0
    def __init__(self, window):
        super(BrowserView, self).__init__()
        BrowserView.instances[window.uid] = self
        self.uid = window.uid
        self.pywebview_window = window

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.window = window

        self.is_fullscreen = False
        self.confirm_close = window.confirm_close
        self.text_select = window.text_select

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)

        self.loaded = window.loaded
        self.shown = window.shown

        self._js_results = {}
        self._current_url = None
        self._file_name = None

        self.resize(window.width, window.height)
        self.title = window.title
        self.setWindowTitle(window.title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(window.background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not window.resizable:
            self.setFixedSize(window.width, window.height)

        self.setMinimumSize(window.min_size[0], window.min_size[1])

        self.frameless = window.frameless
        if self.frameless:
            self.setWindowFlags(QtCore.Qt.Window
                                | QtCore.Qt.FramelessWindowHint)

        self.view = BrowserView.WebView(self)

        if is_webengine:
            os.environ['QTWEBENGINE_CHROMIUM_FLAGS'] = (
                '--use-fake-ui-for-media-stream --enable-features=AutoplayIgnoreWebAudio'
            )

        if _debug and is_webengine:
            # Initialise Remote debugging (need to be done only once)
            if not BrowserView.inspector_port:
                BrowserView.inspector_port = BrowserView._get_free_port()
                os.environ[
                    'QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port
        else:
            self.view.setContextMenuPolicy(
                QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.view.setPage(BrowserView.WebPage(self.view))
        self.view.page().loadFinished.connect(self.on_load_finished)

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.show_trigger.connect(self.on_show_window)
        self.hide_trigger.connect(self.on_hide_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.window_size_trigger.connect(self.on_window_size)
        self.window_move_trigger.connect(self.on_window_move)
        self.window_minimize_trigger.connect(self.on_window_minimize)
        self.window_restore_trigger.connect(self.on_window_restore)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        if is_webengine and platform.system() != 'OpenBSD':
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        if window.fullscreen:
            self.toggle_fullscreen()

        if window.url is not None:
            self.view.setUrl(QtCore.QUrl(window.url))
        elif window.html:
            self.view.setHtml(window.html, QtCore.QUrl(''))
        else:
            self.view.setHtml(default_html, QtCore.QUrl(''))

        if window.x is not None and window.y is not None:
            self.move(window.x, window.y)
        else:
            center = QApplication.desktop().availableGeometry().center(
            ) - self.rect().center()
            self.move(center.x(), center.y())

        if not window.minimized:
            self.activateWindow()
            self.raise_()

        self.shown.set()
 def connect_to_javascript_code(self):
     self.channel = QWebChannel()
     self.handler = CallHandler()
     self.channel.registerObject('handler', self.handler)
     self.page().setWebChannel(self.channel)
     self.loadFinished.connect(self.loadFinishedHandler)
Beispiel #8
0
 def _OnUrlChanged(self, url):
     webchannel = QWebChannel(self.page())
     self.page().setWebChannel(webchannel)
     webchannel.registerObject("kiwoom", self._kiwoom)
Beispiel #9
0
class QtWebContainer(WebContainer):
    def __init__(self,
                 bridge_objs: BridgeObjects = None,
                 *args,
                 **kwargs) -> None:
        super().__init__(name='_web_container',
                         bridge_objs=bridge_objs,
                         *args,
                         **kwargs)

        if self._config.debug_mode:
            os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = '12345'

        self.profile = QWebEngineProfile.defaultProfile()
        self.interceptor = QtUrlRequestInterceptor()
        self.url_scheme_handler = QtUrlSchemeHandler()

        self.view = QWebEngineView(parent=self._main_window.widget)
        self.page = self.view.page()

        self.channel = QWebChannel(self.page)
        self.bridge_initialized = False

        self.profile.installUrlSchemeHandler(b'web-greeter',
                                             self.url_scheme_handler)

        self._initialize_page(self.page)

        if self._config.debug_mode:
            self.devtools = DevTools()

        if not self._config.context_menu.enabled:
            self.view.setContextMenuPolicy(Qt.PreventContextMenu)

        if not self._config.allow_remote_urls:
            self.profile.setRequestInterceptor(self.interceptor)

        if self._config.entry_point.autoload:
            self.initialize_bridge_objects()
            self.load()

        self.view.show()
        self._main_window.widget.setCentralWidget(self.view)

    @staticmethod
    def _create_webengine_script(path: Url, name: str) -> QWebEngineScript:
        script = QWebEngineScript()
        script_file = QFile(path)

        if script_file.open(QFile.ReadOnly):
            script_string = str(script_file.readAll(), 'utf-8')

            script.setInjectionPoint(QWebEngineScript.DocumentCreation)
            script.setName(name)
            script.setWorldId(QWebEngineScript.MainWorld)
            script.setSourceCode(script_string)

        return script

    def _get_channel_api_script(self) -> QWebEngineScript:
        return self._create_webengine_script(':/qtwebchannel/qwebchannel.js',
                                             'QWebChannel API')

    def _init_bridge_channel(self) -> None:
        self.page.setWebChannel(self.channel)
        self.page.scripts().insert(self._get_channel_api_script())

        self.bridge_initialized = True

    def _initialize_page(self, page: QWebEnginePage) -> None:
        page_settings = self.page.settings().globalSettings()

        if self._config.allow_remote_urls:
            ENABLED_SETTINGS.append('LocalContentCanAccessRemoteUrls')
        else:
            DISABLED_SETTINGS.append('LocalContentCanAccessRemoteUrls')

        for setting in DISABLED_SETTINGS:
            try:
                page_settings.setAttribute(
                    getattr(QWebEngineSettings, setting), False)
            except AttributeError:
                pass

        for setting in ENABLED_SETTINGS:
            try:
                page_settings.setAttribute(
                    getattr(QWebEngineSettings, setting), True)
            except AttributeError:
                pass

        page.setView(self.view)

    def initialize_bridge_objects(self) -> None:
        if not self.bridge_initialized:
            self._init_bridge_channel()

        registered_objects = self.channel.registeredObjects()

        for obj in self.bridge_objects:
            if obj not in registered_objects:
                self.channel.registerObject(obj._name, obj)

    def load(self, url: str = '') -> None:
        url = url if url else self._config.entry_point.url

        if 'http' not in url and not os.path.exists(url):
            self.page.setHtml(DEFAULT_ENTRY_POINT)
            return

        if not url.startswith('file'):
            url = 'web-greeter:/{0}'.format(url)

        self.page.load(QUrl(url))

    def load_script(self, path: Url, name: str):
        script = self._create_webengine_script(path, name)
        return self.page.scripts().insert(script)
Beispiel #10
0
            name = self.lineEdit_username.text()
            pwd = self.lineEdit_pwd.text()
            jscode = "PyQt52WebValue('{}','{}');".format(name, pwd)
            self.view.page().runJavaScript(jscode)

    @pyqtSlot()
    def on_pb_reset_clicked(self):
        """
        PyQt5的输入栏内容清空
        """
        self.lineEdit_username.setText("")
        self.lineEdit_pwd.setText("")

    def setLineEdit(self, list):
        self.lineEdit_username.setText(list[0])
        self.lineEdit_pwd.setText(list[1])

    def __del__(self):
        self.view.deleteLater()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    web_pyqt = Web2PyQt5()
    web_pyqt.show()
    channel = QWebChannel()
    shared = Myshared()
    channel.registerObject("connection", shared)
    web_pyqt.view.page().setWebChannel(channel)
    shared.finish[list].connect(web_pyqt.setLineEdit)
    sys.exit(app.exec_())
Beispiel #11
0
import sys
import os
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtWebChannel import QWebChannel
from factorial import Factorial

channel = QWebChannel()
factorial = Factorial()


class PyFactorial(QWidget):
    def __init__(self):
        super(PyFactorial, self).__init__()
        self.setWindowTitle('Python计算阶乘')
        self.resize(600, 500)

        layout = QVBoxLayout()
        self.browser = QWebEngineView()
        url = os.getcwd() + '/templates/test3.html'
        self.browser.load(QUrl.fromLocalFile(url))

        channel.registerObject('obj', factorial)
        self.browser.page().setWebChannel(channel)

        layout.addWidget(self.browser)
        self.setLayout(layout)


if __name__ == '__main__':
Beispiel #12
0
class BrowserView(QMainWindow):
    instances = {}
    inspector_port = None  # The localhost port at which the Remote debugger listens

    create_window_trigger = QtCore.pyqtSignal(object)
    set_title_trigger = QtCore.pyqtSignal(str)
    load_url_trigger = QtCore.pyqtSignal(str)
    html_trigger = QtCore.pyqtSignal(str, str)
    dialog_trigger = QtCore.pyqtSignal(int, str, bool, str, str)
    destroy_trigger = QtCore.pyqtSignal()
    fullscreen_trigger = QtCore.pyqtSignal()
    window_size_trigger = QtCore.pyqtSignal(int, int)
    current_url_trigger = QtCore.pyqtSignal()
    evaluate_js_trigger = QtCore.pyqtSignal(str, str)

    class JSBridge(QtCore.QObject):
        api = None
        parent_uid = None

        try:
            qtype = QtCore.QJsonValue  # QT5
        except AttributeError:
            qtype = str  # QT4

        def __init__(self):
            super(BrowserView.JSBridge, self).__init__()

        @QtCore.pyqtSlot(str, qtype, result=str)
        def call(self, func_name, param):
            func_name = BrowserView._convert_string(func_name)
            param = BrowserView._convert_string(param)

            return _js_bridge_call(self.parent_uid, self.api, func_name, param)

    class WebView(QWebView):
        def __init__(self, parent=None):
            super(BrowserView.WebView, self).__init__(parent)

        def contextMenuEvent(self, event):
            menu = self.page().createStandardContextMenu()

            # If 'Inspect Element' is present in the default context menu, it
            # means the inspector is already up and running.
            for i in menu.actions():
                if i.text() == 'Inspect Element':
                    break
            else:
                # Inspector is not up yet, so create a pseudo 'Inspect Element'
                # menu that will fire it up.
                inspect_element = QAction('Inspect Element')
                inspect_element.triggered.connect(self.show_inspector)
                menu.addAction(inspect_element)

            menu.exec_(event.globalPos())

        # Create a new webview window pointing at the Remote debugger server
        def show_inspector(self):
            uid = self.parent().uid + '-inspector'
            try:
                # If inspector already exists, bring it to the front
                BrowserView.instances[uid].raise_()
                BrowserView.instances[uid].activateWindow()
            except KeyError:
                title = 'Web Inspector - {}'.format(self.parent().title)
                url = 'http://localhost:{}'.format(BrowserView.inspector_port)

                inspector = BrowserView(uid, title, url, 700, 500, True, False,
                                        (300, 200), False, '#fff', False, None,
                                        True,
                                        self.parent().webview_ready)
                inspector.show()

    # New-window-requests handler for Qt 5.5+ only
    class NavigationHandler(QWebPage):
        def __init__(self, parent=None):
            super(BrowserView.NavigationHandler, self).__init__(parent)

        def acceptNavigationRequest(self, url, type, is_main_frame):
            webbrowser.open(url.toString(), 2, True)
            return False

    class WebPage(QWebPage):
        def __init__(self, parent=None):
            super(BrowserView.WebPage, self).__init__(parent)
            self.nav_handler = BrowserView.NavigationHandler(
                self) if _qt_version >= [5, 5] else None

        if _qt_version < [5, 5]:

            def acceptNavigationRequest(self, frame, request, type):
                if frame is None:
                    webbrowser.open(request.url().toString(), 2, True)
                    return False
                return True

        def createWindow(self, type):
            return self.nav_handler

    def __init__(self, uid, title, url, width, height, resizable, fullscreen,
                 min_size, confirm_quit, background_color, debug, js_api,
                 text_select, webview_ready):
        super(BrowserView, self).__init__()
        BrowserView.instances[uid] = self
        self.uid = uid

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.api = js_api
        self.js_bridge.parent_uid = self.uid

        self.is_fullscreen = False
        self.confirm_quit = confirm_quit
        self.text_select = text_select

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)

        self.load_event = Event()
        self.webview_ready = webview_ready

        self._js_results = {}
        self._current_url = None
        self._file_name = None

        self.resize(width, height)
        self.title = title
        self.setWindowTitle(title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not resizable:
            self.setFixedSize(width, height)

        self.setMinimumSize(min_size[0], min_size[1])

        self.view = BrowserView.WebView(self)

        if debug and _qt_version > [5, 5]:
            # Initialise Remote debugging (need to be done only once)
            if not BrowserView.inspector_port:
                BrowserView.inspector_port = BrowserView._get_free_port()
                os.environ[
                    'QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port
        else:
            self.view.setContextMenuPolicy(
                QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.view.setPage(BrowserView.WebPage(self.view))

        if url is not None:
            self.view.setUrl(QtCore.QUrl(url))
        else:
            self.load_event.set()

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.window_size_trigger.connect(self.on_window_size)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        if _qt_version >= [5, 5] and platform.system() != 'OpenBSD':
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        self.view.page().loadFinished.connect(self.on_load_finished)

        if fullscreen:
            self.toggle_fullscreen()

        self.move(QApplication.desktop().availableGeometry().center() -
                  self.rect().center())
        self.activateWindow()
        self.raise_()
        webview_ready.set()

    def on_set_title(self, title):
        self.setWindowTitle(title)

    def on_file_dialog(self, dialog_type, directory, allow_multiple,
                       save_filename, file_filter):
        if dialog_type == FOLDER_DIALOG:
            self._file_name = QFileDialog.getExistingDirectory(
                self,
                localization['linux.openFolder'],
                options=QFileDialog.ShowDirsOnly)
        elif dialog_type == OPEN_DIALOG:
            if allow_multiple:
                self._file_name = QFileDialog.getOpenFileNames(
                    self, localization['linux.openFiles'], directory,
                    file_filter)
            else:
                self._file_name = QFileDialog.getOpenFileName(
                    self, localization['linux.openFile'], directory,
                    file_filter)
        elif dialog_type == SAVE_DIALOG:
            if directory:
                save_filename = os.path.join(str(directory),
                                             str(save_filename))

            self._file_name = QFileDialog.getSaveFileName(
                self, localization['global.saveFile'], save_filename)

        self._file_name_semaphore.release()

    def on_current_url(self):
        url = BrowserView._convert_string(self.view.url().toString())
        self._current_url = None if url == '' else url
        self._current_url_semaphore.release()

    def on_load_url(self, url):
        self.view.setUrl(QtCore.QUrl(url))

    def on_load_html(self, content, base_uri):
        self.view.setHtml(content, QtCore.QUrl(base_uri))

    def closeEvent(self, event):
        if self.confirm_quit:
            reply = QMessageBox.question(
                self, self.title, localization['global.quitConfirmation'],
                QMessageBox.Yes, QMessageBox.No)

            if reply == QMessageBox.No:
                event.ignore()
                return

        event.accept()
        del BrowserView.instances[self.uid]

        try:  # Close inspector if open
            BrowserView.instances[self.uid + '-inspector'].close()
            del BrowserView.instances[self.uid + '-inspector']
        except KeyError:
            pass

        if len(BrowserView.instances) == 0:
            _app.exit()

    def on_destroy_window(self):
        self.close()

    def on_fullscreen(self):
        if self.is_fullscreen:
            self.showNormal()
        else:
            self.showFullScreen()

        self.is_fullscreen = not self.is_fullscreen

    def on_window_size(self, width, height):
        self.setFixedSize(width, height)

    def on_evaluate_js(self, script, uuid):
        def return_result(result):
            result = BrowserView._convert_string(result)
            uuid_ = BrowserView._convert_string(uuid)

            js_result = self._js_results[uuid_]
            js_result[
                'result'] = None if result is None or result == 'null' else result if result == '' else json.loads(
                    result)
            js_result['semaphore'].release()

        try:  # PyQt4
            result = self.view.page().mainFrame().evaluateJavaScript(script)
            return_result(result)
        except AttributeError:  # PyQt5
            self.view.page().runJavaScript(script, return_result)

    def on_load_finished(self):
        if self.js_bridge.api:
            self._set_js_api()
        else:
            self.load_event.set()

        if not self.text_select:
            script = disable_text_select.replace('\n', '')

            try:  # PyQt4
                self.view.page().mainFrame().evaluateJavaScript(script)
            except AttributeError:  # PyQt5
                self.view.page().runJavaScript(script)

    def set_title(self, title):
        self.set_title_trigger.emit(title)

    def get_current_url(self):
        self.load_event.wait()
        self.current_url_trigger.emit()
        self._current_url_semaphore.acquire()

        return self._current_url

    def load_url(self, url):
        self.load_event.clear()
        self.load_url_trigger.emit(url)

    def load_html(self, content, base_uri):
        self.load_event.clear()
        self.html_trigger.emit(content, base_uri)

    def create_file_dialog(self, dialog_type, directory, allow_multiple,
                           save_filename, file_filter):
        self.dialog_trigger.emit(dialog_type, directory, allow_multiple,
                                 save_filename, file_filter)
        self._file_name_semaphore.acquire()

        if _qt_version >= [5, 0]:  # QT5
            if dialog_type == FOLDER_DIALOG:
                file_names = (self._file_name, )
            elif dialog_type == SAVE_DIALOG or not allow_multiple:
                file_names = (self._file_name[0], )
            else:
                file_names = tuple(self._file_name[0])

        else:  # QT4
            if dialog_type == FOLDER_DIALOG:
                file_names = (BrowserView._convert_string(self._file_name), )
            elif dialog_type == SAVE_DIALOG or not allow_multiple:
                file_names = (BrowserView._convert_string(
                    self._file_name[0]), )
            else:
                file_names = tuple(
                    [BrowserView._convert_string(s) for s in self._file_name])

        # Check if we got an empty tuple, or a tuple with empty string
        if len(file_names) == 0 or len(file_names[0]) == 0:
            return None
        else:
            return file_names

    def destroy_(self):
        self.destroy_trigger.emit()

    def toggle_fullscreen(self):
        self.fullscreen_trigger.emit()

    def set_window_size(self, width, height):
        self.window_size_trigger.emit(width, height)

    def evaluate_js(self, script):
        self.load_event.wait()

        result_semaphore = Semaphore(0)
        unique_id = uuid1().hex
        self._js_results[unique_id] = {
            'semaphore': result_semaphore,
            'result': ''
        }

        self.evaluate_js_trigger.emit(script, unique_id)
        result_semaphore.acquire()

        result = deepcopy(self._js_results[unique_id]['result'])
        del self._js_results[unique_id]

        return result

    def _set_js_api(self):
        def _register_window_object():
            frame.addToJavaScriptWindowObject('external', self.js_bridge)

        script = parse_api_js(self.js_bridge.api)

        if _qt_version >= [5, 5]:
            qwebchannel_js = QtCore.QFile('://qtwebchannel/qwebchannel.js')
            if qwebchannel_js.open(QtCore.QFile.ReadOnly):
                source = bytes(qwebchannel_js.readAll()).decode('utf-8')
                self.view.page().runJavaScript(source)
                self.channel.registerObject('external', self.js_bridge)
                qwebchannel_js.close()
        elif _qt_version >= [5, 0]:
            frame = self.view.page().mainFrame()
            _register_window_object()
        else:
            frame = self.view.page().mainFrame()
            _register_window_object()

        try:  # PyQt4
            self.view.page().mainFrame().evaluateJavaScript(script)
        except AttributeError:  # PyQt5
            self.view.page().runJavaScript(script)

        self.load_event.set()

    @staticmethod
    def _convert_string(result):
        try:
            if result is None or result.isNull():
                return None

            result = result.toString()  # QJsonValue conversion
        except AttributeError:
            pass

        return convert_string(result)

    @staticmethod
    # A simple function to obtain an unused localhost port from the os return it
    def _get_free_port():
        s = socket()
        s.bind(('localhost', 0))
        port = str(s.getsockname()[1])
        s.close()
        return port

    @staticmethod
    # Receive func from subthread and execute it on the main thread
    def on_create_window(func):
        func()
Beispiel #13
0
    def __init__(self, uid, title, url, width, height, resizable, fullscreen,
                 min_size, confirm_quit, background_color, debug, js_api,
                 text_select, webview_ready):
        super(BrowserView, self).__init__()
        BrowserView.instances[uid] = self
        self.uid = uid

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.api = js_api
        self.js_bridge.parent_uid = self.uid

        self.is_fullscreen = False
        self.confirm_quit = confirm_quit
        self.text_select = text_select

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)

        self.load_event = Event()
        self.webview_ready = webview_ready

        self._js_results = {}
        self._current_url = None
        self._file_name = None

        self.resize(width, height)
        self.title = title
        self.setWindowTitle(title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not resizable:
            self.setFixedSize(width, height)

        self.setMinimumSize(min_size[0], min_size[1])

        self.view = BrowserView.WebView(self)

        if debug and _qt_version > [5, 5]:
            # Initialise Remote debugging (need to be done only once)
            if not BrowserView.inspector_port:
                BrowserView.inspector_port = BrowserView._get_free_port()
                os.environ[
                    'QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port
        else:
            self.view.setContextMenuPolicy(
                QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.view.setPage(BrowserView.WebPage(self.view))

        if url is not None:
            self.view.setUrl(QtCore.QUrl(url))
        else:
            self.load_event.set()

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.window_size_trigger.connect(self.on_window_size)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        if _qt_version >= [5, 5] and platform.system() != 'OpenBSD':
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        self.view.page().loadFinished.connect(self.on_load_finished)

        if fullscreen:
            self.toggle_fullscreen()

        self.move(QApplication.desktop().availableGeometry().center() -
                  self.rect().center())
        self.activateWindow()
        self.raise_()
        webview_ready.set()
Beispiel #14
0
class Term(QWidget):
    clients = dict()

    def __init__(self, data, open_win=False):
        super(Term, self).__init__()
        self.resize(900, 600)
        self.hv = QHBoxLayout(self)
        # self.setWindowFlags(QtCore.Qt.Widget)
        self.open_win = open_win
        self.QWebEng = QWebEngineView(self)
        self.QWebEng.setObjectName('QWebEng')
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setStyleSheet('background-color:transparent')

        self.setContentsMargins(0, 0, 0, 0)
        # self.setSpacing(0)
        self.QWebEng.resize(400, 400)
        self.hv.addWidget(self.QWebEng)
        self.hv.setContentsMargins(10, 10, 10, 10)
        self.hv.setSpacing(0)
        url = QUrl(QFileInfo("xterm.html").absoluteFilePath())
        self.QWebEng.load(url)
        self.QWebEng.showMaximized()
        # self.QWebEng.setHtml(xterm)
        self.data = data
        self.channel = QWebChannel()
        self.printer = Print()
        self.channel.registerObject('printer', self.printer)
        # self.QWebEng.page().setBackgroundColor(QColor(255, 0, 0, 0))
        self.QWebEng.page().setWebChannel(self.channel)
        # self.QWebEng.page().setBackgroundColor(Qt.transparent)
        self.QWebEng.page().setBackgroundColor(Qt.transparent)
        self.setWindowOpacity(0)

        self.printer.sendf.connect(self.send)
        self.defcmd = "[%s:\~]$ " % os.getcwd().split(':')[0]
        self.Dos_Cmd = b''
        # print(os.path())
        if not self.open_win:
            jscode = "completeAndReturnName('%s');" % json.dumps(
                {'data': base64.b64encode(b'aaaaa').decode('utf-8')})
            # print(jscode)
            #
            self.QWebEng.page().runJavaScript(jscode)
            # self.QWebEng.loadStarted()
        else:
            # self.open_client()
            threading.Thread(target=self.open_client).start()
            # Process(target=self.open_client).start()

    def open_client(self):

        if self.open_win:
            IOST = IOLoop.instance()
            IOST.start()
            bridge = Bridge(IOST, self)
            self.clients[self._id()] = bridge
            self.Start()

    def defalut_client(self, data):
        logger.debug('接受到默认端数据:%s' % data)
        if str(data) == str(b'\x7f'):
            print('退格键')
            print('>>>', data, str(data, 'gbk'),
                  bytes(str(data, 'gbk'), encoding='utf-8'))
            data = b'\x07'
            if len(self.Dos_Cmd) != 0:
                self.Dos_Cmd = self.Dos_Cmd[:-1]
                data = b'\x08\x1b[K'
        elif str(data) == str(b'\r'):
            print('回车键')
            # data = b'\r\n'
            print(self.Dos_Cmd)
            if self.Dos_Cmd == b'clear':
                data = b'\x1b[3;J\x1b[H\x1b[2J\x1b]0;\x07'
            elif self.Dos_Cmd == b'ls':
                self.Dos_Cmd = b'dir'
            else:
                if self.Dos_Cmd != b'':
                    try:
                        proc = subprocess.Popen(str(self.Dos_Cmd, 'utf-8'),
                                                shell=True,
                                                stdout=subprocess.PIPE,
                                                stderr=subprocess.PIPE,
                                                bufsize=-1)
                        std = str(proc.stdout.read(), 'gbk')
                        data = b'\r\n' + bytes((std if std else str(
                            proc.stderr.read(), 'gbk')).replace('\n', '\r\n'),
                                               encoding='utf-8')
                    except Exception as e:
                        print(e)
            data = data + bytes('\r\n' + self.defcmd, encoding='utf-8')
            self.Dos_Cmd = b''
        else:
            self.Dos_Cmd = self.Dos_Cmd + data
        jscode = "completeAndReturnName('%s');" % json.dumps(
            {'data': base64.b64encode(data).decode('utf-8')})
        # jscode = "completeAndReturnName('%s');" % json.dumps({'data': base64.b64encode(b'\n').decode('utf-8')})
        # print(jscode)
        self.QWebEng.page().runJavaScript(jscode)

    def get_client(self):
        return self.clients.get(self._id(), None)

    def _id(self):
        return id(self)

    def Start(self):
        self.bridge = self.get_client()
        logger.debug('打开ssh:%s' % self.data)
        # threading.Thread(target=self.bridge.open,args=).run()
        self.bridge.open(self.data)

    def send(self, data):
        data = json.loads(data)
        if self.open_win:
            logger.debug('发送到服务端数据:%s' % data)
            try:
                self.bridge.SHELL.resize_pty(width=data['cols'],
                                             height=data['rows'])
                if data['data']:
                    self.bridge.trans_forward(data['data'].encode())
            except Exception as E:
                logger.debug('客户端 Error:%s' % str(E))
        else:
            logger.debug('发送到默认数据:%s' % data)
            # Thread(target=self.defalut_client,args=bytes(data['data'], 'utf-8')).start()
            self.defalut_client(bytes(data['data'], 'utf-8'))


# if __name__ == "__main__":
#     app = QApplication(sys.argv)
#     data = {'host': '39.107.100.236', 'port': '1993', 'username': '******', 'ispwd':  True, 'secret': '@Scjz1993'}
#     # data = {'host': '192.168.110.134', 'port': '22', 'username': '******', 'ispwd': True, 'secret': '123456'}
#     # fennbk = Terminal(data={})
#     fennbk = Term(data=data,open_win=True)
#     fennbk.show()
#     sys.exit(app.exec_())
Beispiel #15
0
class PreviewSync(QObject):
    """This class synchronizes the contents of the web and text views and aligns
       them vertically.
    """
    textToPreviewSynced = pyqtSignal()

    # Setup / cleanup
    # ===============
    def __init__(self,
      # The preview dock involved in synchronization.
      previewDock):

        QObject.__init__(self)
        # Only set up sync if fuzzy matching is available.
        if not findApproxTextInTarget:
            return

        # Gather into one variable all the JavaScript needed for PreviewSync.
        self._jsPreviewSync = self._jsOnClick + self._jsWebCursorCoords

        self._dock = previewDock
        self._callbackManager = CallbackManager()
        self._initPreviewToTextSync()
        self._initTextToPreviewSync()
        self._unitTest = False

    def terminate(self):
        # Uninstall the text-to-web sync only if it was installed in the first
        # place (it depends on TRE).
        if findApproxTextInTarget:
            self._cursorMovementTimer.stop()
            # Shut down the background sync. If a sync was already in progress,
            # then discard its output.
            self._runLatest.future.cancel(True)
            self._runLatest.terminate()
            # End all callbacks.
            self._callbackManager.skipAllCallbacks()
            self._callbackManager.waitForAllCallbacks()
            # Bug: DON'T de-register the QWebChannel. This casues the error message ``onmessage is not a callable property of qt.webChannelTransport. Some things might not work as expected.`` to be displayed. I see this in https://code.woboq.org/qt5/qtwebengine/src/core/renderer/web_channel_ipc_transport.cpp.html#221; I assume it's a result of attempting to send a signal to the web page, where the web page doesn't have qwebchannel.js running.
            #self.channel.deregisterObject(self)
            # Delete it. Note that the channel is `NOT owned by the page <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`.
            sip.delete(self.channel)
            # Disconnect all signals.
            sip.delete(self)
    #
    # Vertical synchronization
    ##========================
    # These routines perform vertical synchronization.
    #
    # This function computes the distance, in pixels, measured from the target
    # cursor location to the source cursor location, as shown in part (a) of the
    # figure below: delta = source - target, so that source = target + delta.
    # This distance is limited by a constraint: the resulting target cursor
    # location must be kept a padding pixels amount away from the boundaries of
    # the target widget. Part (b) of the figure shows show this distance is
    # limited when the source lies above the target widget; the same constraint
    # applies when the source lies below the target widget.
    #
    # .. image:: sync_delta.png
    #
    # Ideally, this would instead operate on the baseline of the text, rather
    # than the bottom, but getting this is harder.
    def _alignScrollAmount(self,
      # The top (y) coordinate of the source widget in a global coordinate frame,
      # such as screen coordinates. In pixels.
      sourceGlobalTop,
      # The bottom coordinate of the cursor in the source widget, measured from the
      # top of the widget, NOT the top of the viewport. In pixels.
      sourceCursorBottom,

      # The top (y) coordinate of the target widget in a global coordinate frame,
      # such as screen coordinates. In pixels.
      targetGlobalTop,
      # The bottom coordinate of the cursor in the target widget, measured from the
      # top of the widget, NOT the top of the viewport. In pixels.
      targetCursorBottom,
      # The height of the target widget. In pixels.
      targetHeight,
      # The height of the cursor in the target widget. In pixels.
      targetCursorHeight,
      # The minimum allowable distance between target + delta and the top or
      # bottom of the target widget.
      padding):

        # Compute the raw delta between the source and target widgets.
        #
        # .. image:: dtop_initial_diagram.png
        delta = (
          # Global coords of the source cursor top.
          (sourceGlobalTop + sourceCursorBottom) -
          # Global coords of the target cursor top. The difference
          # gives the number of pixels separating them.
          (targetGlobalTop + targetCursorBottom) );

        # Constrain the resulting delta so that the stays padding pixels from
        # the top of the target widget.
        delta = max(-targetCursorBottom + targetCursorHeight + padding, delta)
        # Likewise, constrain the bottom.
        delta = min(targetHeight - targetCursorBottom - padding, delta)

        return delta

    # This string contains JavaScript code to determine the coordinates and height of the
    # anchor of the selection in the web view.
    _jsWebCursorCoords = (
        # This function returns the [top, left] position in pixels of ``obj``
        # relative to the screen, not to the viewport. This introduces one
        # potential problem: if obj is not visible when this is called, it
        # returns coordinates outside the screen (such that top or left is
        # negative or greater than the screen's height or width.
        #
        # It was slightly modified from http://www.quirksmode.org/js/findpos.html,
        #  which reproduces jQuery's offset method (https://api.jquery.com/offset/).
        'function findPos(obj) {'
            'var curLeft = 0;'
            'var curTop = 0;'
             # element.offsetLeft and element.offsetTop measure relative to
             # the object's parent. Walk the tree of parents, summing each
             # offset to determine the offset from the origin of the web page.
            'do {'
                'curLeft += obj.offsetLeft;'
                'curTop += obj.offsetTop;'
            '} while (obj = obj.offsetParent);'
            # See `element.getBoundingClientRect
            # <https://developer.mozilla.org/en-US/docs/Web/API/element.getBoundingClientRect>`_
            # for converting viewport coords to screen coords.
            'return [curLeft - window.scrollX, curTop - window.scrollY];'
        '}' +

        # This function returns [top, left, width], of the current
        # selection, where:
        #
        #   top, left - coordinates of the anchor of the
        #     selection relative to the screen, in pixels.
        #
        #   height - height at the beginning of the selection, in pixels.
        #
        # Adapted from http://stackoverflow.com/questions/2031518/javascript-selection-range-coordinates.
        # Changes:
        #
        # - jQuery usage eliminated for all but debug prints.
        # - The original code used ``range.endOffset`` instead of
        #   ``selection.focusOffset``. This caused occasional errors when
        #   dragging selections.
        'function selectionAnchorCoords() {'
            # Using ``window.getSelection()``
            # Make sure a `selection <https://developer.mozilla.org/en-US/docs/Web/API/Selection>`_ exists.
            'var selection = window.getSelection();'
            'if (selection.rangeCount == 0) return 0;'

            # The selection can contain not just a point (from a
            # single mouse click) but a range (from a mouse drag or
            # shift+arrow keys).
            # We're looking for the coordinates of the focus node
            # (the place where the mouse ends up after making the selection).
            # However, the range returned by ``selection.getRangeAt(0)``
            # begins earlier in the document and ends later, regardless
            # how the mouse was dragged. So, create a new range containing
            # just the point at the focus node, so we actually get
            # a range pointing to where the mouse is.
            # Ref: `focus <https://developer.mozilla.org/en-US/docs/Web/API/Selection.focusNode>`_ of the selection.
            # `Range <https://developer.mozilla.org/en-US/docs/Web/API/range>`_
            'var rangeAtFocus = document.createRange();'
            'rangeAtFocus.setStart(selection.focusNode, selection.focusOffset);'

            # Insert a measurable element (a span) at the selection's
            # focus.
            'var span = document.createElement("span");'
            'rangeAtFocus.insertNode(span);'

            # Measure coordinates at this span, then remove it.
            'var [left, top] = findPos(span);'
            'var height = span.offsetHeight;'
            'span.remove();'

            'return [left, top, height];'
        '}'

        # Clear the current selection, if it exists.
        'function clearSelection() {'
            'if (window.getSelection()) {'
                'window.getSelection().empty();'
            '}'
        '}'

        # Given text to find, place a highlight on the last line containing the
        # text.
        'function highlightFind('
          # The text to find, typically consisting of all text in the web page
          # from its beginning to the point to be found.
          'txt) {'

            # Clear the current selection, so that a find will start at the
            # beginning of the page.
            'clearSelection();'
            # Find or create a ``div`` used as a highlighter.
            'var highlighter = getHighlight();'
            'if (!highlighter) {'
                'highlighter = document.createElement("div");'
                'document.body.appendChild(highlighter);'
                'highlighter.style.zIndex = 100;'
                'highlighter.style.width = "100%";'
                'highlighter.style.position = "absolute";'
                # Pass any click on the highlight on to the webpage underneath.
                # See https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events.
                'highlighter.style.pointerEvents = "none";'
                'highlighter.style.backgroundColor = "rgba(255, 255, 0, 0.4)";'
                'highlighter.id = "highlighter";'
            '}'
            # See https://developer.mozilla.org/en-US/docs/Web/API/Window/find.
            ##                       aString, aCaseSensitive, aBackwards, aWrapAround, aWholeWord, aSearchInFrames, aShowDialog)
            'var found = window.find(txt,     true,           false,     false,        false,      true,            false);'
            # If the text was found, or the search string was empty, highlight a line.
            'if (found || txt === "") {'
                # Determine the coordiantes of the end of the selection.
                'var res = selectionAnchorCoords();'
                'if (res) {'
                    # Unpack the coordinates obtained.
                    'var [left, top, height] = res;'
                    # Position it based on the coordinates.
                    'highlighter.style.height = height + "px";'
                    'highlighter.style.top = (window.scrollY + top) + "px";'
                '}'
                'return true;'
            '}'
            # Remove the highlight if we can't find the text.
            'clearHighlight();'
            # Clear the selection, since we won't use it later.
            'clearSelection();'
            'return false;'
        '}'

        # Return the ``div`` used to produce a highlight, or None if it doesn't exist.
        'function getHighlight() {'
            'return document.getElementById("highlighter");'
        '}'

        # Delete the element used to produce a highlight.
        'function clearHighlight() {'
            'var highlighter = getHighlight();'
            'if (highlighter) {'
                'highlighter.remove();'
            '}'
        '}')

    # Scroll the web view to align its cursor with the qutepart cursor or vice
    # versa.
    def _scrollSync(self,
      # None to scroll the text view to the y coordinate of the web view's
      # cursor. True or False to do the opposite: scroll the web view so that
      # its cursor aligns vertically with the y coordinate of the text view. In
      # this case, True will use the tolerance to scroll only if the amount to
      # scroll exceeds that tolerance; False will scroll irregardless of the
      # tolerance.
      alreadyScrolling=None,
      # Ignored if ``alreadyScrolling == None``. Used as both a padding value and a
      # scroll tolerance, as described in alreadyScrolling.
      tolerance=50):

        # Per the `window geometry
        # <http://qt-project.org/doc/qt-4.8/application-windows.html#window-geometry>`_,
        # `geometry() <http://qt-project.org/doc/qt-4.8/qwidget.html#geometry-prop>`_
        # is relative to the parent frame. Then, use `mapToGlobal
        # <http://qt-project.org/doc/qt-4.8/qwidget.html#mapToGlobal>`_ to
        # put this in global coordinates. This works for `QWebEngineView
        # <http://doc.qt.io/qt-5/qwebengineview.html>`_, since it
        # inherits from QWidget.
        wv = self._dock._widget.webEngineView
        qp = core.workspace().currentDocument().qutepart
        qpGlobalTop = qp.mapToGlobal(qp.geometry().topLeft()).y()
        wvGlobalTop = wv.mapToGlobal(wv.geometry().topLeft()).y()

        # `qutepart.cursorRect()
        # <http://qt-project.org/doc/qt-4.8/qplaintextedit.html#cursorRect-2>`_
        # gives a value in viewport == widget coordinates. Use that directly.
        cr = qp.cursorRect()
        qpCursorHeight = cr.height()
        qpCursorBottom = cr.top() + qpCursorHeight

        # Widget height includes the scrollbars. Subtract that off to get a
        # viewable height for qutepart.
        qpHeight = qp.geometry().height()
        hsb = qp.horizontalScrollBar()
        # The scrollbar height is a constant, even if it's hidden. So, only
        # include it in calculations if it's visible.
        if hsb.isVisible():
            qpHeight -= qp.horizontalScrollBar().height()
        page = wv.page()
        wvHeight = wv.geometry().height()

        # JavaScript callback to determine the coordinates and height of the
        # anchor of the selection in the web view. It expects a 3-element tuple
        # of (left, top, height), or None if there was no selection, where:
        # top is the coordinate (in pixels) of the top of the selection, measured from the web page's origin;
        # left is the coordinate (in pixels) of the left of the selection, measured from the web page's origin.
        def callback(res):
            # See if a 3-element tuple is returned. Exit if the selection was empty.
            if not res:
                return

            _, wvCursorTop, wvCursorHeight = res
            wvCursorBottom = wvCursorTop + wvCursorHeight

            if alreadyScrolling is not None:
                deltaY = self._alignScrollAmount(qpGlobalTop, qpCursorBottom,
                  wvGlobalTop, wvCursorBottom, wvHeight, wvCursorHeight, tolerance)
                # Uncomment for helpful debug info.
                ##print(("qpGlobalTop = %d, qpCursorBottom = %d, qpHeight = %d, deltaY = %d, tol = %d\n" +
                ##  "  wvGlobalTop = %d, wvCursorBottom = %d, wvHeight = %d, wvCursorHeight = %d") %
                ##  (qpGlobalTop, qpCursorBottom, qpHeight, deltaY, tolerance,
                ##  wvGlobalTop, wvCursorBottom, wvHeight, wvCursorHeight))

                # Only scroll if we've outside the tolerance.
                if alreadyScrolling or (abs(deltaY) > tolerance):
                    # Note that scroll bars are backwards: to make the text go up, you must
                    # move the bars down (a positive delta) and vice versa. Hence, the
                    # subtration, rather than addition, below.
                    page.runJavaScript('window.scrollTo(0, window.scrollY - {});'.format(deltaY))
                # Clear the selection, whether we scrolled or not.
                self.clearSelection()
            else:
                deltaY = self._alignScrollAmount(wvGlobalTop, wvCursorBottom,
                  qpGlobalTop, qpCursorBottom, qpHeight, qpCursorHeight, 0)
                vsb = qp.verticalScrollBar()
                # The units for the vertical scroll bar is pixels, not lines. So, do
                # a kludgy conversion by assuming that all line heights are the
                # same.
                vsb.setValue(vsb.value() - round(deltaY/qpCursorHeight))

        self._dock._afterLoaded.afterLoaded(lambda: page.runJavaScript('selectionAnchorCoords();', QWebEngineScript.ApplicationWorld, self._callbackManager.callback(callback)))

    # Clear the current selection in the web view.
    def clearSelection(self):
        if not self._unitTest:
            self._dock._afterLoaded.afterLoaded(self._dock._widget.webEngineView.page().runJavaScript, 'clearSelection();', QWebEngineScript.ApplicationWorld)
    #
    #
    # Synchronizing between the text pane and the preview pane
    ##========================================================
    # A single click in the preview pane should move the text pane's cursor to the
    # corresponding location. Likewise, movement of the text pane's cursor should
    # select the corresponding text in the preview pane. To do so, an approximate
    # search for text surrounding the current cursor or click location perfomed on
    # text in the other pane provides the corresponding location in the other pane
    # to highlight.
    #
    # Bugs / to-do items
    ##------------------
    # #. I call ``toPlainText()`` several times. In the past, this was quite slow
    #    in a ``QTextEdit``. Check performance and possibly cache this value; it
    #    should be easy to update by adding a few lines to _setHtml().
    #
    # Preview-to-text sync
    ##--------------------
    # This functionaliy relies heavily on the Web to Qt bridge. Some helpful
    # references:
    #
    # * `The QtWebKit Bridge <http://qt-project.org/doc/qt-4.8/qtwebkit-bridge.html>`_
    #   gives a helpful overview.
    # * `QWebEngineView`_ is the top-level widget used to embed a Web page in a Qt
    #   application.
    #
    # For this sync, the first step is to find the single click's location in a
    # plain text rendering of the preview's web content. This is implemented in
    # JavaScript, which emits a Qt signal with the location on a click. A slot
    # connected to this signal then performs the approximate match and updates the
    # text pane's cursor. To do this:
    #
    # #. ``jsClick``, a PyQt signal with a single numeric argument (the index into
    #    a string containing the plain text rendering of the web page) is defined.
    #    This signal is `connected <onJavaScriptCleared.connect>`_ to the
    #    ``onWebviewClick`` slot.
    # #. The ``onJavaScriptCleared`` method inserts the JavaScript to listen for a
    #    click and then emit a signal giving the click's location.
    # #. The ``onWebviewClick`` method then performs the approximate match and
    #    updates the text pane's cursor location.
    # #. When a new web page is loaded, all JavaScript is lost and must be reinserted.
    #    The ``onJavaScriptCleared`` slot, connected to the
    #    ``javaScriptWindowObjectCleared`` signal, does this.
    #
    # The job of this JavaScript handler is to
    # translate a mouse click into an index into the text rendering of the
    # webpage. To do this, we must:
    #
    # #. Get the current selection made by the mouse click, which is typically
    #    an empty range. (I assume a click and drag will produce a non-empty
    #    range; however this code still works).
    # #. Extend a copy of this range so that it begins at the start of the
    #    webpage and, of course, ends at the character nearest the mouse
    #    click.
    # #. Get a string rendering of this range.
    # #. Emit a signal with the length of this string.
    #
    # Note: A JavaScript development environment with this code is available
    # at http://jsfiddle.net/hgDwx/110/.
    _jsOnClick = (

        # The `window.onclick
        # <https://developer.mozilla.org/en-US/docs/Web/API/Window.onclick>`_
        # event is "called when the user clicks the mouse button while the
        # cursor is in the window." Although the docs claim that "this event
        # is fired for any mouse button pressed", I found experimentally
        # that it on fires on a left-click release; middle and right clicks
        # had no effect.
        'function window_onclick() {'

            # Clear the current highlight -- it doesn't make sense to have other
            # text highlighted after a click.
            'clearHighlight();'

             # This performs step 1 above. In particular:
             #
             # - `window.getSelection <https://developer.mozilla.org/en-US/docs/Web/API/Window.getSelection>`_
             #   "returns a `Selection
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Selection>`_
             #   object representing the range of text selected by the
             #   user." Since this is only called after a click, I assume
             #   the Selection object is non-null.
             # - The Selection.\ `getRangeAt <https://developer.mozilla.org/en-US/docs/Web/API/Selection.getRangeAt>`_
             #   method "returns a range object representing one of the
             #   ranges currently selected." Per the Selection `glossary
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary>`_,
             #   "A user will normally only select a single range at a
             #   time..." The index for retrieving a single-selection range
             #   is of course 0.
             # - "The `Range <https://developer.mozilla.org/en-US/docs/Web/API/range>`_
             #   interface represents a fragment of a document that can
             #   contain nodes and parts of text nodes in a given document."
             #   We clone it to avoid modifying the user's existing
             #   selection using `cloneRange
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Range.cloneRange>`_.
            'var r = window.getSelection().getRangeAt(0).cloneRange();'

             # This performs step 2 above: the cloned range is now changed
             # to contain the web page from its beginning to the point where
             # the user clicked by calling `setStartBefore
             # <https://developer.mozilla.org/en-US/docs/Web/API/Range.setStartBefore>`_
             # on `document.body
             # <https://developer.mozilla.org/en-US/docs/Web/API/document.body>`_.
            'r.setStartBefore(document.body);'

             # Step 3:
             #
             # - `cloneContents <https://developer.mozilla.org/en-US/docs/Web/API/Range.cloneContents>`_
             #   "Returns a `DocumentFragment
             #   <https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment>`_
             #   copying the nodes of a Range."
             # - DocumentFragment's parent `Node <https://developer.mozilla.org/en-US/docs/Web/API/Node>`_
             #   provides a `textContent
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Node.textContent>`_
             #   property which gives "a DOMString representing the textual
             #   content of an element and all its descendants." This therefore
             #   contains a text rendering of the webpage from the beginning of the
             #   page to the point where the user clicked.
             'var rStr = r.cloneContents().textContent.toString();'


             # Step 4: the length of the string gives the index of the click
             # into a string containing a text rendering of the webpage.
             # Call Python with the document's text and that index.
            'window.previewSync._onWebviewClick(document.body.textContent.toString(), rStr.length);'
        '}')

    _qtJsInit = (
        # _`Bug 1`: I can't seem to avoid the error message ``js: Uncaught TypeError: channel.execCallbacks[message.id] is not a function``. It seems like this occurs  when a new page is loaded, but the QWebChannel on the Python side sends a message intended for a previously-loaded page. Adding delays / waiting until all JS init finishes / wiating until the event queue is empty helps, but doesn't fix it. Even with these, enough busyness (constant CPU use), this still happends -- perhaps the Python/Qt network backend doesn't send these messages until the CPU is idle? As a workaround, don't define the channel until is needed, making it less likely this will happen.
        #
        # _`Bug 2`: Since ``qt`` may not be defined (Qt 5.7.0 doesn't provide the
        # ``qt`` object to JavaScript when loading per https://bugreports.qt.io/browse/QTBUG-53411),
        # wrap it in a try/except block.
        'function init_qwebchannel() {'
            # Switch event listeners, part 1/2 -- now that this init is done, don't call it again.
            'window.removeEventListener("click", init_qwebchannel);'
            'try {'
                'new QWebChannel(qt.webChannelTransport, function(channel) {'
                    # Save a reference to the previewSync object.
                    'window.previewSync = channel.objects.previewSync;'
                    # Switch event listeners, part 2/2 -- Invoke the usual onclick handler. This will only be run if the QWebChannel init succeeds.
                    'window.addEventListener("click", window_onclick);'
                    # Now that the QWebChannel is ready, use it to handle the click.
                    'window_onclick();'
                '});'
            '} catch (err) {'
                # Re-throw unrecognized errors. When ``qt`` isn't defined,
                # JavaScript reports ``js: Uncaught ReferenceError: qt is not
                # defined``; this works around `bug 2`_.
                'throw err;' #if (!(err instanceof ReferenceError)) throw err;'
            '}'
        '}'
        # Set up the sync system after a click. This works around `bug 1`_. See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener.
        'window.addEventListener("click", init_qwebchannel);'
    )

    def _initPreviewToTextSync(self):
        """Initialize the system per items 1, 2, and 4 above."""
        # When a web page finishes loading, reinsert our JavaScript.
        page = self._dock._widget.webEngineView.page()

        # Insert our scripts into every loaded page.
        qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
        if not qwebchannel_js.open(QIODevice.ReadOnly):
            raise SystemExit(
                'Failed to load qwebchannel.js with error: %s' %
                qwebchannel_js.errorString())
        qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')

        # Set up the QWebChannel. See http://doc.qt.io/qt-5/qtwebchannel-javascript.html.
        # Run the script containing QWebChannel.js first.
        beforeScript = QWebEngineScript()
        beforeScript.setSourceCode(qwebchannel_js + self._jsPreviewSync + self._qtJsInit)
        beforeScript.setName('qwebchannel.js, previewSync')
        # Run this JavaScript separated from any JavaScript present in the loaded web page. This provides better security (rogue pages can't access the QWebChannel) and better isolation (handlers, etc. won't conflict, I hope).
        beforeScript.setWorldId(QWebEngineScript.ApplicationWorld)
        beforeScript.setInjectionPoint(QWebEngineScript.DocumentCreation)
        # Per `setWebChannel <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`_, only one channel is allowed per page. So, don't run this on sub-frames, since it will attempt the creation of more channels for each subframe.
        beforeScript.setRunsOnSubFrames(False)
        page.scripts().insert(beforeScript)

        # Set up the web channel. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html
        # and http://stackoverflow.com/questions/28565254/how-to-use-qt-webengine-and-qwebchannel.
        # For debug, ``set QTWEBENGINE_REMOTE_DEBUGGING=port`` then browse to
        # http://127.0.0.1:port, where port=60000 works for me. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html.
        self.channel = QWebChannel(page)
        self.channel.registerObject("previewSync", self)
        # Expose the ``qt.webChannelTransport`` object in the world where these scripts live.
        page.setWebChannel(self.channel, QWebEngineScript.ApplicationWorld)

    @pyqtSlot(str, int)
    def _onWebviewClick(self, tc, webIndex):
        self._onWebviewClick_(tc, webIndex)
        # Get the qutepart text.
        qp = core.workspace().currentDocument().qutepart
        # Perform an approximate match between the clicked webpage text and the
        # qutepart text.
        textIndex = findApproxTextInTarget(tc, webIndex, qp.text)
        # Move the cursor to textIndex in qutepart, assuming corresponding text
        # was found.
        if textIndex >= 0:
            self._moveTextPaneToIndex(textIndex)

    # Used for testing -- this will be replaced by a mock. Does nothing.
    def _onWebviewClick_(self, tc, webIndex):
        pass

    def _moveTextPaneToIndex(self, textIndex, noWebSync=True):
        """Given an index into the text pane, move the cursor to that index.

        Params:

        - textIndex - The index into the text pane at which to place the cursor.
        - noWebSync - True to prevent the web-to-text sync from running as a
          result of calling this routine.
        """
        # Move the cursor to textIndex.
        qp = core.workspace().currentDocument().qutepart
        cursor = qp.textCursor()
        # Tell the text to preview sync to ignore this cursor position change.
        cursor.setPosition(textIndex, QtGui.QTextCursor.MoveAnchor)
        self._previewToTextSyncRunning = noWebSync
        qp.setTextCursor(cursor)
        self._previewToTextSyncRunning = False
        # Scroll the document to make sure the cursor is visible.
        qp.ensureCursorVisible()
        # Sync the cursors.
        self._scrollSync()
        # Focus on the editor so the cursor will be shown and ready for typing.
        core.workspace().focusCurrentDocument()

    # Text-to-preview sync
    ##--------------------
    # The opposite direction is easier, since all the work can be done in Python.
    # When the cursor moves in the text pane, find its matching location in the
    # preview pane using an approximate match. Select several characters before and
    # after the matching point to make the location more visible, since the preview
    # pane lacks a cursor. Specifically:
    #
    # #. initTextToPreviewSync sets up a timer and connects the _onCursorPositionChanged method.
    # #. _onCursorPositionChanged is called each time the cursor moves. It starts or
    #    resets a short timer. The timer's expiration calls syncTextToWeb.
    # #. syncTextToWeb performs the approximate match, then calls moveWebPaneToIndex
    #    to sync the web pane with the text pane.
    # #. moveWebToPane uses QWebFrame.find to search for the text under the anchor
    #    then select (or highlight) it.

    def _initTextToPreviewSync(self):
        """Called when constructing the PreviewDoc. It performs item 1 above."""
        # Create a timer which will sync the preview with the text cursor a
        # short time after cursor movement stops.
        self._cursorMovementTimer = QTimer()
        self._cursorMovementTimer.setInterval(300)
        self._cursorMovementTimer.timeout.connect(self.syncTextToPreview)
        # Restart this timer every time the cursor moves.
        core.workspace().cursorPositionChanged.connect(self._onCursorPositionChanged)
        # Set up a variable to tell us when the preview to text sync just fired,
        # disabling this sync. Otherwise, that sync would trigger this sync,
        # which is unnecessary.
        self._previewToTextSyncRunning = False
        # Run the approximate match in a separate thread. Cancel it if the
        # document changes.
        self._runLatest = RunLatest('QThread', self)
        self._runLatest.ac.defaultPriority = QThread.LowPriority
        core.workspace().currentDocumentChanged.connect(self._onDocumentChanged)

    def _onDocumentChanged(self, old, new):
        self._runLatest.future.cancel(True)
        self._callbackManager.skipAllCallbacks()
        self._cursorMovementTimer.stop()

    def _onCursorPositionChanged(self):
        """Called when the cursor position in the text pane changes. It (re)schedules
        a text to web sync per item 2 above. Note that the signal connected to
        this slot must be updated when the current document changes, since we only
        want cursor movement notification from the active text document. This is
        handled in _onDocumentChanged.
        """
        # Ignore this callback if a preview to text sync caused it or if the
        # preview dock is closed.
        if not self._previewToTextSyncRunning and self._dock.isVisible():
            self._cursorMovementTimer.stop()
            self._cursorMovementTimer.start()

    def syncTextToPreview(self):
        """When the timer above expires, this is called to sync text to preview
        per item 3 above. It can also be called when a sync is needed (when
        switching windows, for example).
        """
        # Only run this if we TRE is installed.
        if not findApproxTextInTarget:
            return
        # Stop the timer; the next cursor movement will restart it.
        self._cursorMovementTimer.stop()
        # Get a plain text rendering of the web view. Continue execution in a callback.
        qp = core.workspace().currentDocument().qutepart
        qp_text = qp.text
        self._dock._widget.webEngineView.page().toPlainText(
            self._callbackManager.callback(self._havePlainText))

    # Perform an approximate match in a separate thread, then update
    # the cursor based on the match results.
    def _havePlainText(self, html_text):
        # Performance notes: findApproxTextInTarget is REALLY slow. Scrolling
        # through preview.py with profiling enabled produced::
        #
        #  Output from Enki:
        #         41130 function calls in 3.642 seconds
        #
        #   Ordered by: standard name
        #
        #   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        #       13    0.000    0.000    0.000    0.000 __init__.py:406(text)
        #       13    0.000    0.000    3.398    0.261 approx_match.py:138(findApproxText)
        #       13    0.000    0.000    3.432    0.264 approx_match.py:175(findApproxTextInTarget)
        #       13    0.029    0.002    0.034    0.003 approx_match.py:252(refineSearchResult)
        #       26    0.000    0.000    0.000    0.000 core.py:177(workspace)
        #       ...snip lots more 0.000 or very small times...
        #
        # Therefore, finding ways to make this faster or run it in another
        # thread should significantly improve the GUI's responsiveness.
        qp = core.workspace().currentDocument().qutepart
        qp_text = qp.text
        qp_position = qp.textCursor().position()
        self._runLatest.start(self._movePreviewPaneToIndex,
            # Call findApproxTextInTarget, returning the index and the HTML text searched.
            lambda: (findApproxTextInTarget(qp_text, qp_position, html_text), html_text))

    def _movePreviewPaneToIndex(self, future):
        """Highlights webIndex in the preview pane, per item 4 above.

        Params:

        - webIndex - The index to move the cursor / highlight to in the preview
          pane.
        - txt - The text of the webpage, returned by mainFrame.toPlainText().
        """
        # Retrieve the return value from findApproxTextInTarget.
        webIndex, txt = future.result

        view = self._dock._widget.webEngineView
        page = view.page()
        ft = txt[:webIndex]

        def callback(found):
            if found:
                # Sync the cursors.
                self._scrollSync(False)
                self.textToPreviewSynced.emit()

        if webIndex >= 0:
            self._dock._afterLoaded.afterLoaded(lambda: page.runJavaScript('highlightFind({});'.format(repr(ft)), QWebEngineScript.ApplicationWorld, self._callbackManager.callback(callback)))
        else:
            self.clearHighlight()

    def clearHighlight(self):
        self._dock._afterLoaded.afterLoaded(self._dock._widget.webEngineView.page().runJavaScript, 'clearHighlight();', QWebEngineScript.ApplicationWorld)
Beispiel #16
0
from PyQt5.QtWebChannel import  QWebChannel 
import sys


# 创建一个 application实例
app = QApplication(sys.argv)
win = QWidget()
win.setWindowTitle('Web页面中的JavaScript与 QWebEngineView交互例子')

# 创建一个垂直布局器
layout = QVBoxLayout()
win.setLayout(layout)

# 创建一个 QWebEngineView 对象
view =  QWebEngineView()
htmlUrl = 'http://127.0.0.1:8020/web/index.html'
view.load( QUrl( htmlUrl ))

# 创建一个 QWebChannel对象,用来传递pyqt参数到JavaScript
channel =  QWebChannel( )
myObj = MySharedObject()   
channel.registerObject( "bridge", myObj )  
view.page().setWebChannel(channel)
 
# 把QWebView和button加载到layout布局中
layout.addWidget(view)
           
# 显示窗口和运行app
win.show()
sys.exit(app.exec_())
Beispiel #17
0
 def __init__(self, *args, **kwargs):
     super(VarietyPriceWindow, self).__init__(*args, **kwargs)
     """绑定公用属性"""
     self.products = None
     self.db_worker = None
     # 线程是否结束标志位
     self.exchange_lib_thread_over = True
     self.selected_variety_thread_over = True
     """总布局"""
     self.vertical_layout = QVBoxLayout()  # 总纵向布局
     top_select = QHBoxLayout()  # 顶部横向条件选择栏
     """交易所选择下拉框"""
     exchange_label = QLabel('选择交易所:')
     self.exchange_lib = QComboBox()
     self.exchange_lib.activated[str].connect(
         self.exchange_lib_selected)  # 选择交易所调用的方法
     """品种选择下拉框"""
     variety_label = QLabel('选择品种:')
     self.variety_lib = QComboBox()
     self.variety_lib.setMinimumSize(80, 20)
     self.variety_lib.activated[str].connect(
         self.variety_lib_selected)  # 选择品种所调用的方法
     """时间选择"""
     begin_time_label = QLabel("起始日期:")
     self.begin_time = QDateEdit(
         QDate.currentDate().addDays(-2))  # 参数为设置的日期
     self.begin_time.setDisplayFormat('yyyy-MM-dd')  # 时间显示格式
     self.begin_time.setCalendarPopup(True)  # 使用日历选择时间
     end_time_label = QLabel("终止日期:")
     self.end_time = QDateEdit(QDate.currentDate().addDays(-1))  # 参数为设置的日期
     self.end_time.setDisplayFormat('yyyy-MM-dd')  # 时间显示格式
     self.end_time.setCalendarPopup(True)  # 使用日历选择时间
     """确认按钮"""
     self.confirm_button = QPushButton("确定")
     self.confirm_button.clicked.connect(self.confirm)
     """水平布局添加控件"""
     top_select.addWidget(exchange_label)
     top_select.addWidget(self.exchange_lib)
     top_select.addWidget(variety_label)
     top_select.addWidget(self.variety_lib)
     top_select.addWidget(begin_time_label)
     top_select.addWidget(self.begin_time)
     top_select.addWidget(end_time_label)
     top_select.addWidget(self.end_time)
     top_select.addWidget(self.confirm_button)
     """自定义部件"""
     # 工具栏
     self.tools = ToolWidget()
     tool_btn = QPushButton("季节图表")
     self.tool_view_all = QPushButton("返回总览")
     self.tool_view_all.setEnabled(False)
     self.tool_view_all.clicked.connect(self.return_view_all)
     self.tools.addTool(tool_btn)
     self.tools.addTool(self.tool_view_all)
     self.tools.addSpacer()
     tool_btn.clicked.connect(self.season_table)
     # 画布
     self.map_widget = MapWidget()
     # 表格
     self.table_widget = TableWidget(4)
     # 设置格式
     self.table_widget.set_style(
         header_labels=['日期', '价格', '成交量合计', '持仓量合计'])
     """创建QSplitter"""
     self.show_splitter = QSplitter()
     self.show_splitter.setOrientation(Qt.Vertical)  # 垂直拉伸
     # 加入自定义控件
     self.show_splitter.addWidget(self.map_widget)
     self.show_splitter.addWidget(self.table_widget)
     """垂直布局添加布局和控件并设置到窗口"""
     self.vertical_layout.addLayout(top_select)
     self.vertical_layout.addWidget(self.tools)
     self.vertical_layout.addWidget(self.show_splitter)
     # 加入控件
     self.web_view = QWebEngineView()
     self.web_view.load(QUrl("file:///static/html/variety_price.html"))
     self.web_view.page().profile().downloadRequested.connect(
         self.download_requested)  # 页面下载请求(导出excel)
     self.show_splitter.addWidget(self.web_view)
     self.web_view.hide()  # 隐藏,刚开始不可见
     """js交互通道"""
     web_channel = QWebChannel(self.web_view.page())
     self.web_view.page().setWebChannel(web_channel)  # 网页设置信息通道
     web_channel.registerObject("season_table", self.tools)  # 注册信号对象
     web_channel.registerObject("export_table", self)  # 导出表格数据信号对象
     self.season_table_file = None  # 季节表的保存路径
     self.setLayout(self.vertical_layout)
Beispiel #18
0
    def __init__(self, uid, title, url, width, height, resizable, fullscreen,
                 min_size, confirm_quit, background_color, debug, js_api, text_select, frameless, webview_ready):
        super(BrowserView, self).__init__()
        BrowserView.instances[uid] = self
        self.uid = uid

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.api = js_api
        self.js_bridge.parent_uid = self.uid

        self.is_fullscreen = False
        self.confirm_quit = confirm_quit
        self.text_select = text_select

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)

        self.load_event = Event()
        self.webview_ready = webview_ready

        self._js_results = {}
        self._current_url = None
        self._file_name = None

        self.resize(width, height)
        self.title = title
        self.setWindowTitle(title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not resizable:
            self.setFixedSize(width, height)

        self.setMinimumSize(min_size[0], min_size[1])

        self.frameless = frameless
        if frameless:
            self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)

        self.view = BrowserView.WebView(self)

        if debug and is_webengine:
            # Initialise Remote debugging (need to be done only once)
            if not BrowserView.inspector_port:
                BrowserView.inspector_port = BrowserView._get_free_port()
                os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port
        else:
            self.view.setContextMenuPolicy(QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.view.setPage(BrowserView.WebPage(self.view))

        if url is not None:
            self.view.setUrl(QtCore.QUrl(url))
        else:
            self.load_event.set()

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.window_size_trigger.connect(self.on_window_size)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        if is_webengine and platform.system() != 'OpenBSD':
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        self.view.page().loadFinished.connect(self.on_load_finished)

        if fullscreen:
            self.toggle_fullscreen()

        self.move(QApplication.desktop().availableGeometry().center() - self.rect().center())
        self.activateWindow()
        self.raise_()
        webview_ready.set()
    def initLayout(self):
        # ----------------------------left layout---------------------------- #
        self.treeModel = QStandardItemModel()
        self.pageItem = QStandardItem(ROOT_ITEM_PAGE)
        self.pageItem.type = ITEM_TYPE_PAGE_ROOT
        self.buildPageItem()
        self.userobjectItem = QStandardItem(ROOT_ITEM_USER_OBJECT)
        self.userobjectItem.type = ITEM_TYPE_USER_OBJECT_ROOT
        self.buildUserObjectItem()
        self.providerRequestItem = QStandardItem(ROOT_ITEM_PROVIDER_REQUESTS)
        self.providerRequestItem.type = ITEM_TYPE_PROVIDER_REQUESTS_ROOT
        self.pluginItem = QStandardItem(ROOT_ITEM_PLUGIN)
        self.pluginItem.type = ITEM_TYPE_PLUGIN_ROOT
        self.treeModel.appendColumn([
            self.pageItem, self.userobjectItem, self.providerRequestItem,
            self.pluginItem
        ])
        self.treeModel.setHeaderData(0, Qt.Horizontal, 'Model')

        self.treeView = QTreeView()
        self.treeView.setModel(self.treeModel)
        self.treeView.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.treeView.customContextMenuRequested.connect(self.openContextMenu)
        self.treeView.doubleClicked.connect(self.onTreeItemDoubleClicked)
        self.treeView.clicked.connect(self.getDebugData)

        leftContainer = QWidget()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 6, 0)  # left, top, right, bottom
        layout.addWidget(self.treeView)
        leftContainer.setLayout(layout)

        # ----------------------------middle layout---------------------------- #
        middleContainer = QWidget()

        # search shall start not before the user completed typing
        # filter_delay = DelayedExecutionTimer(self)
        # new_column.search_bar.textEdited[str].connect(filter_delay.trigger)
        # filter_delay.triggered[str].connect(self.search)

        self.tabBar = QTabBar()
        self.tabBar.setUsesScrollButtons(False)
        self.tabBar.setDrawBase(False)
        # self.tabBar.addTab('tab1')
        # self.tabBar.addTab('tab2')

        self.pathBar = QWidget()
        layout = QHBoxLayout()
        layout.setAlignment(Qt.AlignLeft)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(1)
        self.pathBar.setLayout(layout)

        self.searchHolder = QWidget()
        layout = QHBoxLayout()
        layout.addWidget(self.tabBar)
        layout.addWidget(self.pathBar)
        layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding))
        self.searchHolder.setLayout(layout)
        self.searchHolder.layout().setContentsMargins(6, 6, 6, 0)

        self.tabContentWidget = QWidget()
        self.browser = QWebEngineView()
        self.browser.setZoomFactor(1.3)
        self.channel = QWebChannel()
        self.webObject = WebShareObject()
        self.channel.registerObject('bridge', self.webObject)
        self.browser.page().setWebChannel(self.channel)
        self.webObject.jsCallback.connect(lambda value: self.addUpdate(value))

        qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
        if not qwebchannel_js.open(QIODevice.ReadOnly):
            raise SystemExit('Failed to load qwebchannel.js with error: %s' %
                             qwebchannel_js.errorString())
        qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')

        script = QWebEngineScript()
        script.setSourceCode(qwebchannel_js)
        script.setInjectionPoint(QWebEngineScript.DocumentCreation)
        script.setName('qtwebchannel.js')
        script.setWorldId(QWebEngineScript.MainWorld)
        script.setRunsOnSubFrames(True)
        self.browser.page().scripts().insert(script)

        Utils.scriptCreator(os.path.join('..', 'resources', 'js', 'event.js'),
                            'event.js', self.browser.page())
        self.browser.page().setWebChannel(self.channel)

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.initQCheckBoxUI())
        layout.addWidget(self.initSearchView())
        layout.addWidget(self.browser)
        self.tabContentWidget.setLayout(layout)
        self.searchWidget.hide()

        middleContainer.stackedWidget = QStackedWidget()
        self.url = XulDebugServerHelper.HOST + 'list-pages'
        self.showXulDebugData(self.url)
        middleContainer.stackedWidget.addWidget(self.tabContentWidget)
        middleContainer.stackedWidget.addWidget(QLabel('tab2 content'))

        self.tabBar.currentChanged.connect(
            lambda: middleContainer.stackedWidget.setCurrentIndex(
                self.tabBar.currentIndex()))

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.searchHolder)
        layout.addWidget(middleContainer.stackedWidget)
        middleContainer.setLayout(layout)

        # ----------------------------right layout---------------------------- #
        self.rightSiderClickInfo = 'Property'

        self.rightSiderTabWidget = QTabWidget()
        self.rightSiderTabBar = QTabBar()
        self.rightSiderTabWidget.setTabBar(self.rightSiderTabBar)
        self.rightSiderTabWidget.setTabPosition(QTabWidget.East)
        self.favoriteTreeView = FavoriteTreeView(self)

        # self.propertyEditor = PropertyEditor(['Key', 'Value'])
        self.inputWidget = UpdateProperty()
        self.rightSiderTabWidget.addTab(self.inputWidget,
                                        IconTool.buildQIcon('property.png'),
                                        'Property')

        self.rightSiderTabWidget.setStyleSheet(
            ('QTab::tab{height:60px;width:32px;color:black;padding:0px}'
             'QTabBar::tab:selected{background:lightgray}'))

        # self.rightSiderTabWidget.addTab(self.propertyEditor,IconTool.buildQIcon('property.png'),'property')
        self.rightSiderTabWidget.addTab(self.favoriteTreeView,
                                        IconTool.buildQIcon('favorites.png'),
                                        'Favorites')
        self.rightSiderTabBar.tabBarClicked.connect(self.rightSiderClick)

        # ----------------------------entire layout---------------------------- #

        self.contentSplitter = QSplitter(Qt.Horizontal)
        self.contentSplitter.setHandleWidth(0)  # thing to grab the splitter

        self.contentSplitter.addWidget(leftContainer)
        self.contentSplitter.addWidget(middleContainer)
        self.contentSplitter.addWidget(self.rightSiderTabWidget)
        self.contentSplitter.setStretchFactor(0, 0)
        self.contentSplitter.setStretchFactor(1, 6)
        self.contentSplitter.setStretchFactor(2, 6)

        self.mainSplitter = QSplitter(Qt.Vertical)
        self.mainSplitter.setHandleWidth(0)

        self.mainSplitter.addWidget(self.contentSplitter)
        self.mainSplitter.addWidget(self.consoleWindow)
        self.mainSplitter.setStretchFactor(1, 0)
        self.mainSplitter.setStretchFactor(2, 1)
        self.setCentralWidget(self.mainSplitter)
        # 默认隐藏掉复选框
        self.groupBox.setHidden(True)
Beispiel #20
0
class MainWindow(QMainWindow):
	WIDTH = 950
	HEIGHT = 600
	expWidget = None
	expName = ''
	hlpName = ''
	hwin = None
	
	uncheckHelpBox = pyqtSignal()
	setEditorText = pyqtSignal(str)
	setConfigText = pyqtSignal(str)
	
	def closeEvent(self, e):
		if self.hwin != None:
			self.hwin.close()

	def __init__(self, lang, app, tr_eyes, tr_qt):
		"""
		The constructor.
		:param lang: the autodetected language, which comes from shell variables
		:type  lang: str
		:param app: pointer to the Application
		:type  app: QApplication
		:param tr_eyes: translator to localize eyes
		:type  tr_eyes: QTranslator
		:param tr_qt: translator to localize Qt5
		:type  tr_qt: QTranslator
		"""
		QMainWindow.__init__(self)
		self.lang=None # this will be set later, after self.translate() tries
		self.app=app
		self.tr_eyes=tr_eyes
		self.tr_qt=tr_qt

		self.conf = configparser.ConfigParser()
		self.conf.read(cnf)
		
		try:
			self.translate(self.conf['ScreenTheme']['language'])
			self.lang = self.conf['ScreenTheme']['language']
		except:
			self.translate(lang)
			self.lang=lang
			
		self.init_UI()
		self.uncheckHelpBox.connect(self.uncheckTheHelpBox)
		#self.setEditorText.connect(self.updateEditor)
		#self.setConfigText.connect(self.updateConfig)

	def uncheckTheHelpBox(self):
		"""
		unchecks the help checkbox
		"""
		self.helpCB.setChecked(False)
		return

	def init_UI(self):
		self.makeMenu()
		self.setMinimumSize(self.WIDTH-100, self.HEIGHT-50)
		self.resize(self.WIDTH,self.HEIGHT)
		self._x = 100
		self._y = 10
		palette = QPalette()								# background color
		palette.setColor(QPalette.Background, QColor(81,188,185)) #("#99ccff")) "#88bbcc"
		self.setPalette(palette)	

		self.helpCB = QCheckBox(self.tr('Enable PopUp Help Window'))
		self.helpCB.stateChanged.connect(self.showHelp)
		#self.helpCB.setStyleSheet('background-color: white')

		self.statusBar = QStatusBar()
		self.setStatusBar(self.statusBar)
		self.statusBar.addWidget(self.helpCB)
		
		self.callExpt(electronicsExptsScope[0])					# Start the scope by default
		self.screen = QDesktopWidget().screenGeometry()
		self.show()
		self.move(20, 20)

	def showHelp(self):
		if self.helpCB.isChecked() == True:
			if self.hwin == None:
				self.hwin = helpWin(self, (self.title,self.hlpName), self.lang)
				if(self.hlpName[1] in ['editor','advanced_logger']):
					try:
						from PyQt5.QtWebChannel import QWebChannel
						self.channel = QWebChannel()
						self.handler = self.editorHandler(self.setEditorText,self.setConfigText)
						self.channel.registerObject('handler', self.handler)
						self.hwin.page().setWebChannel(self.channel)
					except Exception as e:
						print(e)

			self.hwin.show()
		else:
			if self.hwin != None: self.hwin.hide()
	
	
	def scope_help(self,e):
		self.hlpName = e[1]
		if self.expName != 'scope':
			explib = importlib.import_module('scope')
			try:
				if self.expWidget != None:
					self.expWidget.timer.stop()     # Stop the timer loop of current widget			
				self.hwin = None
				self.expWidget= None 			    # Let python delete it
				w = explib.Expt(p) 
				self.setCentralWidget(w)
				self.expWidget = w
				self.expName = 'scope'
			except:
				self.expName = ''
				self.setWindowTitle(self.tr('Failed to load scope'))
		self.setWindowTitle(self.tr(e[0]))
		self.hwin = None
		self.title = e[0]
		self.showHelp()
	

	def callExpt(self, e):
		"""
		:parm: e lst with a title and a HTML file designation; when e[1]
		is not a string, then it is an iterable with possible HTML file names,
		and the last file name may also be a module name.
		"""	
		module_name =  e[1] if type(e[1]) is str else e[1][-1]
		explib = importlib.import_module(module_name)
		try:
			if self.expWidget != None:
				self.expWidget.timer.stop()	 # Stop the timer loop of current widget			
			self.hwin = None
			self.expWidget= None				 # Let python delete it
			w = explib.Expt(p) 
			self.setWindowTitle(self.tr(e[0]))
			self.setCentralWidget(w)
			self.expWidget = w
			self.expName = e[1]
			self.hlpName = e[1]
			self.title = e[0]
			self.showHelp()
		except Exception as err:
			print("Exception:", err)	
			self.expName = ''
			self.setWindowTitle(unicode(self.tr('Failed to load %s')) %e[0])
		return
		
	def runCode(self, e):
		if self.expName != 'editor': #Moved here from some other non coding expt
			self.hlpName = e
			self.callExpt( ['Python Coding', ('9.0','editor')])
		self.expWidget.mycode = e[1]
		self.expWidget.update()

	def setConfig(self,section, key, value):
		"""
		Sets some part of eyes's configuration
		@param section a section of the configuration file cnf, for
		example: 'ScreenTheme'
		@param key for example: 'Background'
		@param value the text to assign to the key, for example: 'dark'
		"""
		self.conf = configparser.ConfigParser()
		self.conf.read(cnf)
		self.conf[section][key] = value
		with open(cnf,"w") as out: self.conf.write(out)
		return
	
	def setWBG(self):
		"""
		sets a light background for the scope's screen
		"""	
		self.setConfig('ScreenTheme', 'Background', 'light')
		QMessageBox.warning(
			self,
			self.tr('No immediate application'),
			self.tr("Please restart the application to lighten the screen's background")
		)
		return
		
	def setBBG(self):
		"""
		sets a dark background for the scope's screen
		"""	
		self.setConfig('ScreenTheme', 'Background', 'dark')
		QMessageBox.warning(
			self,
			self.tr('No immediate application'),
			self.tr("Please restart the application to darken the screen's background.")
		)
		return
	
	def makeMenu(self):
		imagePath = os.path.join(os.path.dirname(
			os.path.abspath(__file__)),'images')
		bar = self.menuBar()
		bar.clear() # reset all menu actions
		mb = bar.addMenu(self.tr("Device"))
		mb.addAction(self.tr('Reconnect'), self.reconnect)
		mb.addAction(self.tr('LightBackGround next time'), self.setWBG)
		mb.addAction(self.tr('DarkBackGround next time'), self.setBBG)
		sm = mb.addMenu(self.tr("Choose Language"))
		sm.setIcon(QIcon(os.path.join(imagePath, "UN_emblem_blue.svg")))
		for e in languages:
			action = sm.addAction(str(e),  lambda item=str(e): self.setLanguage(item))
			flag=e.flag(imagePath)
			if flag:
				action.setIcon(QIcon(flag))
				action.setIconVisibleInMenu(True)
		em = bar.addMenu(self.tr("School Expts"))
		for e in schoolExpts:
			em.addAction(self.tr(e[0]),  lambda item=e: self.scope_help(item))	

		em = bar.addMenu(self.tr("Electronics"))
		for e in electronicsExptsScope:
			em.addAction(self.tr(e[0]),  lambda item=e: self.scope_help(item))	
			
		for e in electronicsExpts:
			em.addAction(self.tr(e[0]),  lambda item=e: self.callExpt(item))	
		
		em = bar.addMenu(self.tr("Electrical"))
		for e in electricalExpts:
			em.addAction(self.tr(e[0]),  lambda item=e: self.callExpt(item))	

		em = bar.addMenu(self.tr("Sound"))
		for e in soundExpts:
			em.addAction(self.tr(e[0]),  lambda item=e: self.callExpt(item))	

		em = bar.addMenu(self.tr("Mechanics"))
		for e in mechanicsExpts:
			em.addAction(self.tr(e[0]),  lambda item=e: self.callExpt(item))	

		em = bar.addMenu(self.tr("Other Expts"))
		for e in otherExpts:
			em.addAction(self.tr(e[0]),  lambda item=e: self.callExpt(item))	

	def setLanguage(self,l):
			self.setConfig('ScreenTheme', 'language', l)
			self.translators=self.translate(l)
			self.lang=l
			self.init_UI()
			return

	def reconnect(self):
		global p,eyes
		try:
			p.H.disconnect()
		except:
			pass
		p=eyes.open()
		if self.expWidget is None:
			explib = importlib.import_module('scope')
			self.expWidget = explib.Expt(p) 
			self.setCentralWidget(self.expWidget)
			self.setWindowTitle(self.tr('Oscilloscope'))
			self.expName = 'scope'

		self.expWidget.p = p
		self.expWidget.msg('')
		if p != None: 
			print('recovering...',self.expName)
			if self.expName == 'scope':
				self.expWidget.recover()
		
	# translation stuff
	def translate(self, lang = None):
		try:
			self.app.removeTranslator(self.tr_eyes)
			self.app.removeTranslator(self.tr_qt)
		except:
			pass

		if lang is None:
			lang=QLocale.system().name()
		self.tr_eyes=QTranslator()
		self.tr_eyes.load("lang/"+lang, os.path.dirname(__file__))
		self.app.installTranslator(self.tr_eyes)
		self.tr_qt=QTranslator()
		self.tr_qt.load("qt_"+lang,
			QLibraryInfo.location(QLibraryInfo.TranslationsPath))
		self.app.installTranslator(self.tr_qt)
		self.uncheckHelpBox.emit()
class MainWindow(BaseWindow):
    def __init__(self):
        super().__init__()
        self.qObject = QObject()
        self.initConsole()
        self.initUI()
        self.show()

    def initConsole(self):
        self.consoleWindow = ButtomWindow()

    def initUI(self):
        self.resize(int(Utils.getWindowWidth() * 0.8),
                    int(Utils.getWindowHeight() * 0.8))
        self.initMenuBar()
        self.initLayout()
        super().initWindow()

    def initMenuBar(self):
        menuBar = self.menuBar()
        fileMenu = menuBar.addMenu('File')
        disConnectAction = QAction(IconTool.buildQIcon('disconnect.png'),
                                   '&Disconnect', self)
        disConnectAction.setShortcut('Ctrl+D')
        disConnectAction.setShortcutContext(Qt.ApplicationShortcut)
        disConnectAction.triggered.connect(self.restartProgram)
        settingAction = QAction(IconTool.buildQIcon('setting.png'),
                                '&Setting...', self)
        settingAction.setShortcut('Ctrl+Shift+S')
        settingAction.setShortcutContext(Qt.ApplicationShortcut)
        settingAction.triggered.connect(lambda: STCLogger().d('setting'))
        clearCacheAction = QAction(IconTool.buildQIcon('clearCache.png'),
                                   '&ClearCache', self)
        clearCacheAction.setShortcut('Ctrl+Alt+C')
        clearCacheAction.setShortcutContext(Qt.ApplicationShortcut)
        clearCacheAction.triggered.connect(self.clearCache)
        clearSearchHistoryAction = QAction(
            IconTool.buildQIcon('clearCache.png'), '&ClearSearchHistory', self)
        clearSearchHistoryAction.triggered.connect(self.clearSearchHistory)
        settingLogPathAction = QAction(IconTool.buildQIcon('path.png'),
                                       '&LogPath', self)
        settingLogPathAction.triggered.connect(self.setLogPath)
        fileMenu.addAction(disConnectAction)
        fileMenu.addAction(clearCacheAction)
        fileMenu.addAction(settingLogPathAction)
        fileMenu.addAction(clearSearchHistoryAction)
        # fileMenu.addAction(settingAction)
        # fileMenu.addAction(showLogAction)

        # settingAction.triggered.connect(self.openSettingWindow)

        editMenu = menuBar.addMenu('Edit')
        findAction = QAction(IconTool.buildQIcon('find.png'), '&Find', self)
        findAction.setShortcut('Ctrl+F')
        findAction.triggered.connect(self.findActionClick)
        editMenu.addAction(findAction)

        focusItemAction = QAction(IconTool.buildQIcon('focus.png'),
                                  '&Focus Item', self)
        focusItemAction.triggered.connect(self.focusChooseItem)
        editMenu.addAction(focusItemAction)
        self.chooseItemType = ''
        self.chooseItemId = ''

        settingMenu = menuBar.addMenu('Setting')
        autoLoginAction = QAction(IconTool.buildQIcon('setting.png'),
                                  '&Auto Login', self)
        autoLoginAction.setShortcutContext(Qt.ApplicationShortcut)
        autoLoginAction.triggered.connect(self.setAutoState)
        settingMenu.addAction(autoLoginAction)

        helpMenu = menuBar.addMenu('Help')
        aboutAction = QAction(IconTool.buildQIcon('about.png'), '&About', self)
        aboutAction.triggered.connect(self.openAboutWindow)
        helpMenu.addAction(aboutAction)

    def closeEvent(self, e):
        self.settings.setValue('searchHistory', self.searchHistory)
        e.accept()

    def openAboutWindow(self):
        print('open about')
        self.aboutWindow = AboutWindow()
        self.aboutWindow.show()

    def setAutoState(self):
        reply = QMessageBox.question(self, "提示", "是否取消自动登录功能",
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)
        if reply == QMessageBox.Yes:
            Utils.setAutoLoginState(False)

    def restartProgram(self):
        from XulDebugTool.ui.ConnectWindow import ConnectWindow  # 不应该在这里导入,但是放在前面会有问题
        try:
            hasAbout = object.__getattribute__(self, "aboutWindow")
        except:
            hasAbout = None
        if hasAbout is not None:
            self.aboutWindow.close()
        print("新建连接页面")
        self.con = ConnectWindow()
        self.close()

    # def openSettingWindow(self):
    #     self.tableInfoModel = SettingWindow()
    #     self.tableInfoModel.show()

    def initLayout(self):
        # ----------------------------left layout---------------------------- #
        self.treeModel = QStandardItemModel()
        self.pageItem = QStandardItem(ROOT_ITEM_PAGE)
        self.pageItem.type = ITEM_TYPE_PAGE_ROOT
        self.buildPageItem()
        self.userobjectItem = QStandardItem(ROOT_ITEM_USER_OBJECT)
        self.userobjectItem.type = ITEM_TYPE_USER_OBJECT_ROOT
        self.buildUserObjectItem()
        self.providerRequestItem = QStandardItem(ROOT_ITEM_PROVIDER_REQUESTS)
        self.providerRequestItem.type = ITEM_TYPE_PROVIDER_REQUESTS_ROOT
        self.pluginItem = QStandardItem(ROOT_ITEM_PLUGIN)
        self.pluginItem.type = ITEM_TYPE_PLUGIN_ROOT
        self.treeModel.appendColumn([
            self.pageItem, self.userobjectItem, self.providerRequestItem,
            self.pluginItem
        ])
        self.treeModel.setHeaderData(0, Qt.Horizontal, 'Model')

        self.treeView = QTreeView()
        self.treeView.setModel(self.treeModel)
        self.treeView.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.treeView.customContextMenuRequested.connect(self.openContextMenu)
        self.treeView.doubleClicked.connect(self.onTreeItemDoubleClicked)
        self.treeView.clicked.connect(self.getDebugData)

        leftContainer = QWidget()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 6, 0)  # left, top, right, bottom
        layout.addWidget(self.treeView)
        leftContainer.setLayout(layout)

        # ----------------------------middle layout---------------------------- #
        middleContainer = QWidget()

        # search shall start not before the user completed typing
        # filter_delay = DelayedExecutionTimer(self)
        # new_column.search_bar.textEdited[str].connect(filter_delay.trigger)
        # filter_delay.triggered[str].connect(self.search)

        self.tabBar = QTabBar()
        self.tabBar.setUsesScrollButtons(False)
        self.tabBar.setDrawBase(False)
        # self.tabBar.addTab('tab1')
        # self.tabBar.addTab('tab2')

        self.pathBar = QWidget()
        layout = QHBoxLayout()
        layout.setAlignment(Qt.AlignLeft)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(1)
        self.pathBar.setLayout(layout)

        self.searchHolder = QWidget()
        layout = QHBoxLayout()
        layout.addWidget(self.tabBar)
        layout.addWidget(self.pathBar)
        layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding))
        self.searchHolder.setLayout(layout)
        self.searchHolder.layout().setContentsMargins(6, 6, 6, 0)

        self.tabContentWidget = QWidget()
        self.browser = QWebEngineView()
        self.browser.setZoomFactor(1.3)
        self.channel = QWebChannel()
        self.webObject = WebShareObject()
        self.channel.registerObject('bridge', self.webObject)
        self.browser.page().setWebChannel(self.channel)
        self.webObject.jsCallback.connect(lambda value: self.addUpdate(value))

        qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
        if not qwebchannel_js.open(QIODevice.ReadOnly):
            raise SystemExit('Failed to load qwebchannel.js with error: %s' %
                             qwebchannel_js.errorString())
        qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')

        script = QWebEngineScript()
        script.setSourceCode(qwebchannel_js)
        script.setInjectionPoint(QWebEngineScript.DocumentCreation)
        script.setName('qtwebchannel.js')
        script.setWorldId(QWebEngineScript.MainWorld)
        script.setRunsOnSubFrames(True)
        self.browser.page().scripts().insert(script)

        Utils.scriptCreator(os.path.join('..', 'resources', 'js', 'event.js'),
                            'event.js', self.browser.page())
        self.browser.page().setWebChannel(self.channel)

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.initQCheckBoxUI())
        layout.addWidget(self.initSearchView())
        layout.addWidget(self.browser)
        self.tabContentWidget.setLayout(layout)
        self.searchWidget.hide()

        middleContainer.stackedWidget = QStackedWidget()
        self.url = XulDebugServerHelper.HOST + 'list-pages'
        self.showXulDebugData(self.url)
        middleContainer.stackedWidget.addWidget(self.tabContentWidget)
        middleContainer.stackedWidget.addWidget(QLabel('tab2 content'))

        self.tabBar.currentChanged.connect(
            lambda: middleContainer.stackedWidget.setCurrentIndex(
                self.tabBar.currentIndex()))

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.searchHolder)
        layout.addWidget(middleContainer.stackedWidget)
        middleContainer.setLayout(layout)

        # ----------------------------right layout---------------------------- #
        self.rightSiderClickInfo = 'Property'

        self.rightSiderTabWidget = QTabWidget()
        self.rightSiderTabBar = QTabBar()
        self.rightSiderTabWidget.setTabBar(self.rightSiderTabBar)
        self.rightSiderTabWidget.setTabPosition(QTabWidget.East)
        self.favoriteTreeView = FavoriteTreeView(self)

        # self.propertyEditor = PropertyEditor(['Key', 'Value'])
        self.inputWidget = UpdateProperty()
        self.rightSiderTabWidget.addTab(self.inputWidget,
                                        IconTool.buildQIcon('property.png'),
                                        'Property')

        self.rightSiderTabWidget.setStyleSheet(
            ('QTab::tab{height:60px;width:32px;color:black;padding:0px}'
             'QTabBar::tab:selected{background:lightgray}'))

        # self.rightSiderTabWidget.addTab(self.propertyEditor,IconTool.buildQIcon('property.png'),'property')
        self.rightSiderTabWidget.addTab(self.favoriteTreeView,
                                        IconTool.buildQIcon('favorites.png'),
                                        'Favorites')
        self.rightSiderTabBar.tabBarClicked.connect(self.rightSiderClick)

        # ----------------------------entire layout---------------------------- #

        self.contentSplitter = QSplitter(Qt.Horizontal)
        self.contentSplitter.setHandleWidth(0)  # thing to grab the splitter

        self.contentSplitter.addWidget(leftContainer)
        self.contentSplitter.addWidget(middleContainer)
        self.contentSplitter.addWidget(self.rightSiderTabWidget)
        self.contentSplitter.setStretchFactor(0, 0)
        self.contentSplitter.setStretchFactor(1, 6)
        self.contentSplitter.setStretchFactor(2, 6)

        self.mainSplitter = QSplitter(Qt.Vertical)
        self.mainSplitter.setHandleWidth(0)

        self.mainSplitter.addWidget(self.contentSplitter)
        self.mainSplitter.addWidget(self.consoleWindow)
        self.mainSplitter.setStretchFactor(1, 0)
        self.mainSplitter.setStretchFactor(2, 1)
        self.setCentralWidget(self.mainSplitter)
        # 默认隐藏掉复选框
        self.groupBox.setHidden(True)

    def addUpdate(self, value=None):
        self.inputWidget.initData(self.pageId, value)
        self.inputWidget.updateItemUI()
        dict = json.loads(value)
        if dict['action'] == "click":
            self.chooseItemId = dict['Id']
            self.chooseItemType = Utils.findNodeById(dict['Id'],
                                                     dict['xml']).tag
        elif dict['action'] == "load":
            self.browser.load(QUrl(dict['url']))
        else:
            pass

    def focusChooseItem(self):
        if self.chooseItemType in ('area', 'item'):
            XulDebugServerHelper.focusChooseItemUrl(self.chooseItemId)

    def initQCheckBoxUI(self):
        self.groupBox = QGroupBox()
        self.skipPropCheckBox = QCheckBox(SKIP_PROP, self)
        self.skipPropCheckBox.setChecked(False)
        self.skipPropCheckBox.stateChanged.connect(
            lambda: self.clickCheckBox(self.skipPropCheckBox, SKIP_PROP))

        self.withChildrenCheckBox = QCheckBox(WITH_CHILDREN, self)
        self.withChildrenCheckBox.setChecked(False)
        self.withChildrenCheckBox.stateChanged.connect(
            lambda: self.clickCheckBox(self.withChildrenCheckBox, WITH_CHILDREN
                                       ))

        self.withBindingDataCheckBox = QCheckBox(WITH_BINDING_DATA, self)
        self.withBindingDataCheckBox.setChecked(False)
        self.withBindingDataCheckBox.stateChanged.connect(
            lambda: self.clickCheckBox(self.withBindingDataCheckBox,
                                       WITH_BINDING_DATA))

        self.withPositionCheckBox = QCheckBox(WITH_POSITION, self)
        self.withPositionCheckBox.setChecked(False)
        self.withPositionCheckBox.stateChanged.connect(
            lambda: self.clickCheckBox(self.withPositionCheckBox, WITH_POSITION
                                       ))

        self.withSelectorCheckBox = QCheckBox(WITH_SELECTOR, self)
        self.withSelectorCheckBox.setChecked(False)
        self.withSelectorCheckBox.stateChanged.connect(
            lambda: self.clickCheckBox(self.withSelectorCheckBox, WITH_SELECTOR
                                       ))

        checkGrouplayout = QHBoxLayout()
        checkGrouplayout.addWidget(self.skipPropCheckBox)
        checkGrouplayout.addWidget(self.withChildrenCheckBox)
        checkGrouplayout.addWidget(self.withBindingDataCheckBox)
        checkGrouplayout.addWidget(self.withPositionCheckBox)
        checkGrouplayout.addWidget(self.withSelectorCheckBox)
        self.groupBox.setLayout(checkGrouplayout)
        return self.groupBox

    def initSearchView(self):
        self.settings = QSettings('XulDebugTool')
        self.searchHistory = self.settings.value('searchHistory', [])

        self.searchWidget = QWidget()
        self.searchWidget.setStyleSheet(
            ".QWidget{border:1px solid rgb(220, 220, 220)}")
        searchPageLayout = QHBoxLayout()
        self.searchLineEdit = QLineEdit()

        self.searchIcon = QAction(self)
        self.searchIcon.setIcon(IconTool.buildQIcon('find_h.png'))

        self.searchMenu = QMenu(self)
        for text in self.searchHistory:
            self.action = QAction(
                text,
                self,
                triggered=lambda: self.searchLineEdit.setText(text))
            self.searchMenu.addAction(self.action)
        self.searchIcon.setMenu(self.searchMenu)

        self.searchDelIcon = QAction(self)
        self.searchDelIcon.setIcon(IconTool.buildQIcon('del.png'))

        self.searchLineEdit.addAction(self.searchIcon,
                                      QLineEdit.LeadingPosition)
        self.searchLineEdit.addAction(self.searchDelIcon,
                                      QLineEdit.TrailingPosition)
        self.searchDelIcon.setVisible(False)
        self.searchLineEdit.setStyleSheet(
            "border:2px groove gray;border-radius:10px;padding:2px 4px")

        searchPageLayout.addWidget(self.searchLineEdit)
        self.searchLineEdit.textChanged.connect(self.searchPage)
        self.searchLineEdit.editingFinished.connect(self.saveSearchHistory)
        self.searchDelIcon.triggered.connect(self.searchDelClick)
        self.previousBtn = QPushButton()
        self.previousBtn.setStyleSheet("background:transparent;")
        self.previousBtn.setIcon(IconTool.buildQIcon('up.png'))
        self.previousBtn.setFixedSize(15, 20)
        searchPageLayout.addWidget(self.previousBtn)
        self.previousBtn.clicked.connect(
            lambda: self.previousBtnClick(self.searchLineEdit.text()))
        self.nextBtn = QPushButton()
        self.nextBtn.setIcon(IconTool.buildQIcon('down.png'))
        self.nextBtn.setStyleSheet("background:transparent;")
        self.nextBtn.setFixedSize(15, 20)
        self.nextBtn.clicked.connect(
            lambda: self.nextBtnClick(self.searchLineEdit.text()))
        searchPageLayout.addWidget(self.nextBtn)
        self.matchCase = QCheckBox("Match Case")
        self.matchCase.setChecked(False)
        self.matchCase.stateChanged.connect(self.matchCaseChange)
        searchPageLayout.addWidget(self.matchCase)

        self.matchTips = QLabel()
        self.matchTips.setFixedWidth(100)
        self.searchClose = QPushButton("×")
        self.searchClose.setFixedWidth(10)
        self.searchClose.setStyleSheet("background:transparent;")
        self.searchClose.clicked.connect(self.searchCloseClick)
        searchPageLayout.addWidget(self.matchTips)
        searchPageLayout.addWidget(self.searchClose)
        self.searchWidget.setLayout(searchPageLayout)
        return self.searchWidget

    def clickCheckBox(self, checkBox, name):
        if checkBox.isChecked():
            STCLogger().i('select ' + name)
            self.selectCheckBoxInfo(name)
        else:
            STCLogger().i('cancel ' + name)
            self.cancelCheckBoxInfo(name)

    def selectCheckBoxInfo(self, str):
        if None != self.url:
            checkedStr = str + '=' + 'true'
            unCheckedStr = str + '=' + 'false'
            if self.url.find('?') == -1:
                self.url += '?'
                self.url += checkedStr
            else:
                if self.url.find(str) == -1:
                    self.url += '&'
                    self.url += checkedStr
                elif self.url.find(unCheckedStr) != -1:
                    self.url = self.url.replace(unCheckedStr, checkedStr)
            self.showXulDebugData(self.url)

    def cancelCheckBoxInfo(self, str):
        if None != self.url:
            checkedStr = str + '=' + 'true'
            if self.url.find(checkedStr) >= -1:
                split = self.url.split(checkedStr)
                self.url = ''.join(split)
                self.url = self.url.replace('&&', '&')
                self.url = self.url.replace('?&', '?')
                if self.url.endswith('?'):
                    self.url = self.url[:-1]
                if self.url.endswith('&'):
                    self.url = self.url[:-1]
                self.showXulDebugData(self.url)

    def rightSiderClick(self, index):
        # 两次单击同一个tabBar时显示隐藏内容区域
        if self.rightSiderTabBar.tabText(index) == self.rightSiderClickInfo:
            if self.rightSiderTabWidget.width() == Utils.getItemHeight():
                self.rightSiderTabWidget.setMaximumWidth(
                    Utils.getWindowWidth())
                self.rightSiderTabWidget.setMinimumWidth(Utils.getItemHeight())
            else:
                self.rightSiderTabWidget.setFixedWidth(Utils.getItemHeight())
        else:
            if self.rightSiderTabWidget.width() == Utils.getItemHeight():
                self.rightSiderTabWidget.setMaximumWidth(
                    Utils.getWindowWidth())
                self.rightSiderTabWidget.setMinimumWidth(Utils.getItemHeight())
        self.rightSiderClickInfo = self.rightSiderTabBar.tabText(index)

    def clearCache(self):
        r = XulDebugServerHelper.clearAllCaches()
        if r.status == 200:
            self.statusBar().showMessage('cache cleanup success')
        else:
            self.statusBar().showMessage('cache cleanup failed')

    def setLogPath(self):
        file_path = QFileDialog.getSaveFileName(self, 'save file',
                                                ConfigHelper.LOGCATPATH,
                                                "Txt files(*.txt)")
        if len(file_path[0]) > 0:
            ConfigurationDB.saveConfiguration(ConfigHelper.KEY_LOGCATPATH,
                                              file_path[0])

    @pyqtSlot(QPoint)
    def openContextMenu(self, point):
        index = self.treeView.indexAt(point)
        if not index.isValid():
            return
        item = self.treeModel.itemFromIndex(index)
        menu = QMenu()

        if item.type == ITEM_TYPE_PROVIDER:
            queryAction = QAction(
                IconTool.buildQIcon('data.png'),
                '&Query Data...',
                self,
                triggered=lambda: self.showQueryDialog(item.data))
            queryAction.setShortcut('Alt+Q')
            menu.addAction(queryAction)

        copyAction = QAction(
            IconTool.buildQIcon('copy.png'),
            '&Copy',
            self,
            triggered=lambda: pyperclip.copy('%s' % index.data()))
        copyAction.setShortcut(QKeySequence.Copy)
        menu.addAction(copyAction)
        menu.exec_(self.treeView.viewport().mapToGlobal(point))

    @pyqtSlot(QModelIndex)
    def getDebugData(self, index):
        item = self.treeModel.itemFromIndex(index)

        if item.type == ITEM_TYPE_PAGE_ROOT:  # 树第一层,page节点
            self.buildPageItem()
            self.url = XulDebugServerHelper.HOST + 'list-pages'
            self.showXulDebugData(self.url)
        elif item.type == ITEM_TYPE_USER_OBJECT_ROOT:  # 树第一层,userObject节点
            self.buildUserObjectItem()
            self.url = XulDebugServerHelper.HOST + 'list-user-objects'
            self.showXulDebugData(self.url)
        elif item.type == ITEM_TYPE_PROVIDER_REQUESTS_ROOT:  # 树第一层,Provider Request节点
            self.url = XulDebugServerHelper.HOST + 'list-provider-requests'
            self.showXulDebugData(self.url)
        elif item.type == ITEM_TYPE_PLUGIN_ROOT:  # 树第一层,plugin节点
            pass
        elif item.type == ITEM_TYPE_PAGE:  # 树第二层,page下的子节点
            pageId = item.id
            self.url = XulDebugServerHelper.HOST + 'get-layout/' + pageId
            self.showXulDebugData(self.url)
        elif item.type == ITEM_TYPE_USER_OBJECT:  # 树第二层,userObject下的子节点
            objectId = item.id
            self.url = XulDebugServerHelper.HOST + 'get-user-object/' + objectId
            self.showXulDebugData(self.url)
        elif item.type == ITEM_TYPE_PROVIDER:  # 树第三层,userObject下的DataService下的子节点
            pass

        self.groupBox.setHidden(item.type != ITEM_TYPE_PAGE)
        # self.fillPropertyEditor(item.data)

    @pyqtSlot(QModelIndex)
    def onTreeItemDoubleClicked(self, index):
        item = self.treeModel.itemFromIndex(index)
        if item.type == ITEM_TYPE_PROVIDER:
            self.showQueryDialog(item.data)

    def buildPageItem(self):
        self.pageItem.removeRows(0, self.pageItem.rowCount())
        r = XulDebugServerHelper.listPages()
        if r:
            pagesNodes = Utils.xml2json(r.data, 'pages')
            if pagesNodes == '':
                return
            # 如果只有一个page,转化出来的json不是数据.分开处理
            if isinstance(pagesNodes['page'], list):
                for i, page in enumerate(pagesNodes['page']):
                    # 把page解析了以后放page节点下
                    row = QStandardItem(page['@pageId'])
                    row.id = self.pageId = page['@id']
                    row.data = page
                    row.type = ITEM_TYPE_PAGE
                    self.pageItem.appendRow(row)
            else:
                page = pagesNodes['page']
                row = QStandardItem(page['@pageId'])
                row.id = self.pageId = page['@id']
                row.data = page
                row.type = ITEM_TYPE_PAGE
                self.pageItem.appendRow(row)
            if self.pageItem.rowCount() > 0:
                self.pageItem.setText(
                    '%s(%s)' % (ROOT_ITEM_PAGE, self.pageItem.rowCount()))

    def buildUserObjectItem(self):
        self.userobjectItem.removeRows(0, self.userobjectItem.rowCount())
        r = XulDebugServerHelper.listUserObject()
        if r:
            userObjectNodes = Utils.xml2json(r.data, 'objects')
            # 如果只有一个userObject,转化出来的json不是数据.分开处理
            if userObjectNodes and isinstance(userObjectNodes['object'], list):
                for i, o in enumerate(userObjectNodes['object']):
                    # 把userObject加到User-Object节点下
                    row = QStandardItem(o['@name'])
                    row.id = o['@id']
                    row.data = o
                    row.type = ITEM_TYPE_USER_OBJECT
                    self.userobjectItem.appendRow(row)
                    # 如果是DataServcie, 填充所有的Provider到该节点下
                    if o['@name'] == CHILD_ITEM_DATA_SERVICE:
                        r = XulDebugServerHelper.getUserObject(o['@id'])
                        if r:
                            dataServiceNodes = Utils.xml2json(r.data, 'object')
                            if isinstance(
                                    dataServiceNodes['object']['provider'],
                                    list):
                                for j, provider in enumerate(
                                        dataServiceNodes['object']
                                    ['provider']):
                                    dsRow = QStandardItem(
                                        provider['ds']['@providerClass'])
                                    dsRow.id = provider['@name']
                                    dsRow.data = provider
                                    dsRow.type = ITEM_TYPE_PROVIDER
                                    row.appendRow(dsRow)
                            else:
                                provider = dataServiceNodes['object'][
                                    'provider']
                                dsRow = QStandardItem(
                                    provider['ds']['@providerClass'])
                                dsRow.id = provider['@name']
                                dsRow.data = provider
                                dsRow.type = ITEM_TYPE_PROVIDER
                                row.appendRow(dsRow)
                            # 对Provider按升序排序
                            row.sortChildren(0)
                    if row.rowCount() > 0:
                        row.setText('%s(%s)' % (row.text(), row.rowCount()))
            else:
                # 没有只有一个userObject的情况, 暂不处理
                pass
        if self.userobjectItem.rowCount() > 0:
            self.userobjectItem.setText(
                '%s(%s)' %
                (ROOT_ITEM_USER_OBJECT, self.userobjectItem.rowCount()))

    def showXulDebugData(self, url):
        STCLogger().i('request url:' + url)
        self.browser.load(QUrl(url))
        self.statusBar().showMessage(url)

    def convertProperty(self, k, v):
        """递归的将多层属性字典转成单层的."""
        if isinstance(v, dict):
            for subk, subv in v.items():
                self.convertProperty(subk, subv)
        else:
            setattr(self.qObject, k, v)

    def showQueryDialog(self, data):
        STCLogger().i('show query dialog: ', data)
        self.dialog = DataQueryDialog(data)
        self.dialog.finishSignal.connect(self.onGetQueryUrl)
        self.dialog.show()

    def onGetQueryUrl(self, url):
        STCLogger().i('request url:' + url)
        self.favoriteTreeView.updateTree()
        self.browser.load(QUrl(url))
        self.statusBar().showMessage(url)

    def findActionClick(self):
        self.searchWidget.show()
        self.searchLineEdit.setFocus()
        self.searchLineEdit.setText(self.browser.selectedText())

    def searchPage(self, text):
        if not text.strip():
            self.searchDelIcon.setVisible(False)
        else:
            self.searchDelIcon.setVisible(True)
        check = self.matchCase.isChecked()
        if check:
            self.browser.findText(text, QWebEnginePage.FindFlags(2),
                                  lambda result: self.changeMatchTip(result))
        else:
            self.browser.findText(text, QWebEnginePage.FindFlags(0),
                                  lambda result: self.changeMatchTip(result))

    def saveSearchHistory(self):
        text = self.searchLineEdit.text()
        if text != '' and text not in self.searchHistory:
            self.action = QAction(
                text,
                self,
                triggered=lambda: self.searchLineEdit.setText(text))
            self.searchMenu.addAction(self.action)
            self.searchHistory.append(text)

    def clearSearchHistory(self):
        self.searchHistory = []

    def previousBtnClick(self, text):
        check = self.matchCase.isChecked()
        if check:
            self.browser.findText(
                text,
                QWebEnginePage.FindFlags(1) | QWebEnginePage.FindFlags(2),
                lambda result: self.changeMatchTip(result))
        else:
            self.browser.findText(text, QWebEnginePage.FindFlags(1),
                                  lambda result: self.changeMatchTip(result))

    def nextBtnClick(self, text):
        check = self.matchCase.isChecked()
        if check:
            self.browser.findText(text, QWebEnginePage.FindFlags(2),
                                  lambda result: self.changeMatchTip(result))
        else:
            self.browser.findText(text, QWebEnginePage.FindFlags(0),
                                  lambda result: self.changeMatchTip(result))

    def matchCaseChange(self):
        self.browser.findText("")
        self.searchPage(self.searchLineEdit.text())

    def changeMatchTip(self, result):
        if result:
            self.matchTips.setText("Find matches")
        else:
            self.matchTips.setText("No matches")

    def searchDelClick(self):
        self.searchLineEdit.setText("")
        self.browser.findText("")
        self.matchTips.setText("")

    def searchCloseClick(self):
        self.searchLineEdit.setText("")
        self.browser.findText("")
        self.matchTips.setText("")
        self.searchWidget.hide()
Beispiel #22
0
class BrowserView(QMainWindow):
    instances = {}
    inspector_port = None  # The localhost port at which the Remote debugger listens

    create_window_trigger = QtCore.pyqtSignal(object)
    set_title_trigger = QtCore.pyqtSignal(str)
    load_url_trigger = QtCore.pyqtSignal(str)
    html_trigger = QtCore.pyqtSignal(str, str)
    dialog_trigger = QtCore.pyqtSignal(int, str, bool, str, str)
    destroy_trigger = QtCore.pyqtSignal()
    hide_trigger = QtCore.pyqtSignal()
    show_trigger = QtCore.pyqtSignal()
    fullscreen_trigger = QtCore.pyqtSignal()
    window_size_trigger = QtCore.pyqtSignal(int, int)
    window_move_trigger = QtCore.pyqtSignal(int, int)
    window_minimize_trigger = QtCore.pyqtSignal()
    window_restore_trigger = QtCore.pyqtSignal()
    current_url_trigger = QtCore.pyqtSignal()
    evaluate_js_trigger = QtCore.pyqtSignal(str, str)

    class JSBridge(QtCore.QObject):
        qtype = QtCore.QJsonValue if is_webengine else str

        def __init__(self):
            super(BrowserView.JSBridge, self).__init__()

        @QtCore.pyqtSlot(str, qtype, str, result=str)
        def call(self, func_name, param, value_id):
            func_name = BrowserView._convert_string(func_name)
            param = BrowserView._convert_string(param)

            return js_bridge_call(self.window, func_name, param, value_id)

    class WebView(QWebView):
        def __init__(self, parent=None):
            super(BrowserView.WebView, self).__init__(parent)

            if parent.frameless:
                QApplication.instance().installEventFilter(self)
                self.setMouseTracking(True)

        def contextMenuEvent(self, event):
            menu = self.page().createStandardContextMenu()

            # If 'Inspect Element' is present in the default context menu, it
            # means the inspector is already up and running.
            for i in menu.actions():
                if i.text() == 'Inspect Element':
                    break
            else:
                # Inspector is not up yet, so create a pseudo 'Inspect Element'
                # menu that will fire it up.
                inspect_element = QAction('Inspect Element', menu)
                inspect_element.triggered.connect(self.show_inspector)
                menu.addAction(inspect_element)

            menu.exec_(event.globalPos())

        # Create a new webview window pointing at the Remote debugger server
        def show_inspector(self):
            uid = self.parent().uid + '-inspector'
            try:
                # If inspector already exists, bring it to the front
                BrowserView.instances[uid].raise_()
                BrowserView.instances[uid].activateWindow()
            except KeyError:
                title = 'Web Inspector - {}'.format(self.parent().title)
                url = 'http://localhost:{}'.format(BrowserView.inspector_port)
                window = Window('web_inspector', title, url, '', 700, 500,
                                None, None, True, False, (300, 200), False,
                                False, False, False, '#fff', None, False)

                inspector = BrowserView(window)
                inspector.show()

        def mousePressEvent(self, event):
            if event.button() == QtCore.Qt.LeftButton:
                self.drag_pos = event.globalPos() - self.parent(
                ).frameGeometry().topLeft()

            event.accept()

        def mouseMoveEvent(self, event):
            if self.parent().frameless and int(
                    event.buttons()) == 1:  # left button is pressed
                self.parent().move(event.globalPos() - self.drag_pos)

        def eventFilter(self, object, event):
            if object.parent() == self:
                if event.type() == QtCore.QEvent.MouseMove:
                    self.mouseMoveEvent(event)
                elif event.type() == QtCore.QEvent.MouseButtonPress:
                    self.mousePressEvent(event)

            return False

    # New-window-requests handler for Qt 5.5+ only
    class NavigationHandler(QWebPage):
        def __init__(self, parent=None):
            super(BrowserView.NavigationHandler, self).__init__(parent)

        def acceptNavigationRequest(self, url, type, is_main_frame):
            webbrowser.open(url.toString(), 2, True)
            return False

    class WebPage(QWebPage):
        def __init__(self, parent=None):
            super(BrowserView.WebPage, self).__init__(parent)
            if is_webengine:
                self.featurePermissionRequested.connect(
                    self.onFeaturePermissionRequested)
                self.nav_handler = BrowserView.NavigationHandler(self)
            else:
                self.nav_handler = None

        if is_webengine:

            def onFeaturePermissionRequested(self, url, feature):
                if feature in (
                        QWebPage.MediaAudioCapture,
                        QWebPage.MediaVideoCapture,
                        QWebPage.MediaAudioVideoCapture,
                ):
                    self.setFeaturePermission(url, feature,
                                              QWebPage.PermissionGrantedByUser)
                else:
                    self.setFeaturePermission(url, feature,
                                              QWebPage.PermissionDeniedByUser)
        else:

            def acceptNavigationRequest(self, frame, request, type):
                if frame is None:
                    webbrowser.open(request.url().toString(), 2, True)
                    return False
                return True

        def createWindow(self, type):
            return self.nav_handler

    def __init__(self, window):
        super(BrowserView, self).__init__()
        BrowserView.instances[window.uid] = self
        self.uid = window.uid
        self.pywebview_window = window

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.window = window

        self.is_fullscreen = False
        self.confirm_close = window.confirm_close
        self.text_select = window.text_select

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)

        self.loaded = window.loaded
        self.shown = window.shown

        self._js_results = {}
        self._current_url = None
        self._file_name = None

        self.resize(window.width, window.height)
        self.title = window.title
        self.setWindowTitle(window.title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(window.background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not window.resizable:
            self.setFixedSize(window.width, window.height)

        self.setMinimumSize(window.min_size[0], window.min_size[1])

        self.frameless = window.frameless
        if self.frameless:
            self.setWindowFlags(QtCore.Qt.Window
                                | QtCore.Qt.FramelessWindowHint)

        self.view = BrowserView.WebView(self)

        if is_webengine:
            os.environ['QTWEBENGINE_CHROMIUM_FLAGS'] = (
                '--use-fake-ui-for-media-stream --enable-features=AutoplayIgnoreWebAudio'
            )

        if _debug and is_webengine:
            # Initialise Remote debugging (need to be done only once)
            if not BrowserView.inspector_port:
                BrowserView.inspector_port = BrowserView._get_free_port()
                os.environ[
                    'QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port
        else:
            self.view.setContextMenuPolicy(
                QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.view.setPage(BrowserView.WebPage(self.view))
        self.view.page().loadFinished.connect(self.on_load_finished)

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.show_trigger.connect(self.on_show_window)
        self.hide_trigger.connect(self.on_hide_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.window_size_trigger.connect(self.on_window_size)
        self.window_move_trigger.connect(self.on_window_move)
        self.window_minimize_trigger.connect(self.on_window_minimize)
        self.window_restore_trigger.connect(self.on_window_restore)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        if is_webengine and platform.system() != 'OpenBSD':
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        if window.fullscreen:
            self.toggle_fullscreen()

        if window.url is not None:
            self.view.setUrl(QtCore.QUrl(window.url))
        elif window.html:
            self.view.setHtml(window.html, QtCore.QUrl(''))
        else:
            self.view.setHtml(default_html, QtCore.QUrl(''))

        if window.x is not None and window.y is not None:
            self.move(window.x, window.y)
        else:
            center = QApplication.desktop().availableGeometry().center(
            ) - self.rect().center()
            self.move(center.x(), center.y())

        if not window.minimized:
            self.activateWindow()
            self.raise_()

        self.shown.set()

    def on_set_title(self, title):
        self.setWindowTitle(title)

    def on_file_dialog(self, dialog_type, directory, allow_multiple,
                       save_filename, file_filter):
        if dialog_type == FOLDER_DIALOG:
            self._file_name = QFileDialog.getExistingDirectory(
                self,
                localization['linux.openFolder'],
                options=QFileDialog.ShowDirsOnly)
        elif dialog_type == OPEN_DIALOG:
            if allow_multiple:
                self._file_name = QFileDialog.getOpenFileNames(
                    self, localization['linux.openFiles'], directory,
                    file_filter)
            else:
                self._file_name = QFileDialog.getOpenFileName(
                    self, localization['linux.openFile'], directory,
                    file_filter)
        elif dialog_type == SAVE_DIALOG:
            if directory:
                save_filename = os.path.join(str(directory),
                                             str(save_filename))

            self._file_name = QFileDialog.getSaveFileName(
                self, localization['global.saveFile'], save_filename)

        self._file_name_semaphore.release()

    def on_current_url(self):
        url = BrowserView._convert_string(self.view.url().toString())
        self._current_url = None if url == '' or url.startswith(
            'data:text/html') else url
        self._current_url_semaphore.release()

    def on_load_url(self, url):
        self.view.setUrl(QtCore.QUrl(url))

    def on_load_html(self, content, base_uri):
        self.view.setHtml(content, QtCore.QUrl(base_uri))

    def closeEvent(self, event):
        self.pywebview_window.closing.set()
        if self.confirm_close:
            reply = QMessageBox.question(
                self, self.title, localization['global.quitConfirmation'],
                QMessageBox.Yes, QMessageBox.No)

            if reply == QMessageBox.No:
                event.ignore()
                return

        event.accept()
        del BrowserView.instances[self.uid]

        if self.pywebview_window in windows:
            windows.remove(self.pywebview_window)

        try:  # Close inspector if open
            BrowserView.instances[self.uid + '-inspector'].close()
            del BrowserView.instances[self.uid + '-inspector']
        except KeyError:
            pass

        self.pywebview_window.closed.set()

        if len(BrowserView.instances) == 0:
            self.hide()
            _app.exit()

    def on_show_window(self):
        self.show()

    def on_hide_window(self):
        self.hide()

    def on_destroy_window(self):
        self.close()

    def on_fullscreen(self):
        if self.is_fullscreen:
            self.showNormal()
        else:
            self.showFullScreen()

        self.is_fullscreen = not self.is_fullscreen

    def on_window_size(self, width, height):
        self.setFixedSize(width, height)

    def on_window_move(self, x, y):
        self.move(x, y)

    def on_window_minimize(self):
        self.setWindowState(QtCore.Qt.WindowMinimized)

    def on_window_restore(self):
        self.setWindowState(QtCore.Qt.WindowNoState)
        self.raise_()
        self.activateWindow()

    def on_evaluate_js(self, script, uuid):
        def return_result(result):
            result = BrowserView._convert_string(result)
            uuid_ = BrowserView._convert_string(uuid)

            js_result = self._js_results[uuid_]
            js_result[
                'result'] = None if result is None or result == 'null' else result if result == '' else json.loads(
                    result)
            js_result['semaphore'].release()

        try:  # < Qt5.6
            result = self.view.page().mainFrame().evaluateJavaScript(script)
            return_result(result)
        except AttributeError:
            self.view.page().runJavaScript(script, return_result)
        except Exception as e:
            logger.exception(e)

    def on_load_finished(self):
        self._set_js_api()

        if not self.text_select:
            script = disable_text_select.replace('\n', '')

            try:  # QT < 5.6
                self.view.page().mainFrame().evaluateJavaScript(script)
            except AttributeError:
                self.view.page().runJavaScript(script)

    def set_title(self, title):
        self.set_title_trigger.emit(title)

    def get_current_url(self):
        self.loaded.wait()
        self.current_url_trigger.emit()
        self._current_url_semaphore.acquire()

        return self._current_url

    def load_url(self, url):
        self.loaded.clear()
        self.load_url_trigger.emit(url)

    def load_html(self, content, base_uri):
        self.loaded.clear()
        self.html_trigger.emit(content, base_uri)

    def create_file_dialog(self, dialog_type, directory, allow_multiple,
                           save_filename, file_filter):
        self.dialog_trigger.emit(dialog_type, directory, allow_multiple,
                                 save_filename, file_filter)
        self._file_name_semaphore.acquire()

        if dialog_type == FOLDER_DIALOG:
            file_names = (self._file_name, )
        elif dialog_type == SAVE_DIALOG or not allow_multiple:
            file_names = (self._file_name[0], )
        else:
            file_names = tuple(self._file_name[0])

        # Check if we got an empty tuple, or a tuple with empty string
        if len(file_names) == 0 or len(file_names[0]) == 0:
            return None
        else:
            return file_names

    def hide_(self):
        self.hide_trigger.emit()

    def show_(self):
        self.show_trigger.emit()

    def destroy_(self):
        self.destroy_trigger.emit()

    def toggle_fullscreen(self):
        self.fullscreen_trigger.emit()

    def resize_(self, width, height):
        self.window_size_trigger.emit(width, height)

    def move_window(self, x, y):
        self.window_move_trigger.emit(x, y)

    def minimize(self):
        self.window_minimize_trigger.emit()

    def restore(self):
        self.window_restore_trigger.emit()

    def evaluate_js(self, script):
        self.loaded.wait()
        result_semaphore = Semaphore(0)
        unique_id = uuid1().hex
        self._js_results[unique_id] = {
            'semaphore': result_semaphore,
            'result': ''
        }

        self.evaluate_js_trigger.emit(script, unique_id)
        result_semaphore.acquire()

        result = deepcopy(self._js_results[unique_id]['result'])
        del self._js_results[unique_id]

        return result

    def _set_js_api(self):
        def _register_window_object():
            frame.addToJavaScriptWindowObject('external', self.js_bridge)

        code = 'qtwebengine' if is_webengine else 'qtwebkit'
        script = parse_api_js(self.js_bridge.window.js_api, code)

        if is_webengine:
            qwebchannel_js = QtCore.QFile('://qtwebchannel/qwebchannel.js')
            if qwebchannel_js.open(QtCore.QFile.ReadOnly):
                source = bytes(qwebchannel_js.readAll()).decode('utf-8')
                self.view.page().runJavaScript(source)
                self.channel.registerObject('external', self.js_bridge)
                qwebchannel_js.close()
        else:
            frame = self.view.page().mainFrame()
            _register_window_object()

        try:  # < QT 5.6
            self.view.page().mainFrame().evaluateJavaScript(script)
        except AttributeError:
            self.view.page().runJavaScript(script)

        self.loaded.set()

    @staticmethod
    def _convert_string(result):
        try:
            if result is None or result.isNull():
                return None

            result = result.toString()  # QJsonValue conversion
        except AttributeError:
            pass

        return convert_string(result)

    @staticmethod
    # A simple function to obtain an unused localhost port from the os return it
    def _get_free_port():
        s = socket()
        s.bind(('localhost', 0))
        port = str(s.getsockname()[1])
        s.close()
        return port

    @staticmethod
    # Receive func from subthread and execute it on the main thread
    def on_create_window(func):
        func()
Beispiel #23
0
    #Este se usa para abrir localmente
    config.mainWindow.abrirLocal.clicked.connect(window.callLocal)
    config.mainWindow.abrirLocal_2.clicked.connect(window.callLocal)

    config.mainWindow.btnSyncDestinos.clicked.connect(destinamelos)

    config.mainWindow.btnFiltroFechas.clicked.connect(
        interfacePochteca.manejador)

    config.mainWindow.btnLogin_3.clicked.connect(validateLogin)

    config.mainWindow.progress_fn(0, "Sesión no iniciada")

    window.dashboardMotor = QWebEngineView(window.mapFrame)
    p = WebPage()
    window.dashboardMotor.setPage(p)
    window.dashboardMotor.setGeometry(0, 0,
                                      window.mapFrame.frameGeometry().width(),
                                      window.mapFrame.frameGeometry().height())
    p.profile().scripts().insert(client_script())
    c = QWebChannel(p)
    p.setWebChannel(c)
    c.registerObject('bridge', p)
    window.dashboardMotor.setHtml(
        '<!DOCTYPE html> <html> <head> <meta name="viewport" content="initial-scale=1.0, user-scalable=no" /> <style type="text/css"> html { height: 100% } body { height: 100%; margin: 0; padding: 0 } #map-canvas { height: 100% } </style> <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"> </script> <script type="text/javascript"> function palPy(arg){new QWebChannel(qt.webChannelTransport, function(channel) { channel.objects.bridge.print(arg); });}  function initialize() { var myLatlng = new google.maps.LatLng(19.358322,-99.2775786); var mapOptions = { center: myLatlng, zoom: 16 }; var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions); var marker = new google.maps.Marker({ position: myLatlng, map: map, title: "Hello World!" }); google.maps.event.addListener(map, "center_changed", function() { window.setTimeout(function() { var center = map.getCenter(); marker.setPosition(center); console.log(center.toString()); palPy(center.toString()); window.status = center.lat(); }, 100); }); } google.maps.event.addDomListener(window, "load", initialize); </script> </head> <body><div id="map-canvas"/></body></html>'
    )

    window.show()
    window.dashboardMotor.setGeometry(0, 0, 1272, 300)
    sys.exit(app.exec_())
Beispiel #24
0
    def __init__(self, uid, title, url, width, height, resizable, fullscreen,
                 min_size, confirm_quit, background_color, debug, js_api,
                 webview_ready):
        super(BrowserView, self).__init__()
        BrowserView.instances[uid] = self
        self.uid = uid

        self.is_fullscreen = False
        self.confirm_quit = confirm_quit

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore()
        self._evaluate_js_semaphore = Semaphore(0)
        self.load_event = Event()

        self._evaluate_js_result = None
        self._current_url = None
        self._file_name = None

        self.resize(width, height)
        self.title = title
        self.setWindowTitle(title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not resizable:
            self.setFixedSize(width, height)

        self.setMinimumSize(min_size[0], min_size[1])

        self.view = QWebView(self)

        if url is not None:
            self.view.setUrl(QtCore.QUrl(url))

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.api = js_api
        self.js_bridge.parent_uid = self.uid

        if _qt_version >= [5, 5]:
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        self.view.page().loadFinished.connect(self.on_load_finished)

        if fullscreen:
            self.toggle_fullscreen()

        self.view.setContextMenuPolicy(
            QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.move(QApplication.desktop().availableGeometry().center() -
                  self.rect().center())
        self.activateWindow()
        self.raise_()
        webview_ready.set()
Beispiel #25
0
class PreviewSync(QObject):
    """This class synchronizes the contents of the web and text views and aligns
       them vertically.
    """
    textToPreviewSynced = pyqtSignal()

    # Setup / cleanup
    # ===============
    def __init__(self,
      # The preview dock involved in synchronization.
      previewDock):

        QObject.__init__(self)
        # Only set up sync if fuzzy matching is available.
        if not findApproxTextInTarget:
            return

        # Gather into one variable all the JavaScript needed for PreviewSync.
        self._jsPreviewSync = self._jsOnClick + self._jsWebCursorCoords

        self._dock = previewDock
        self._callbackManager = CallbackManager()
        self._initPreviewToTextSync()
        self._initTextToPreviewSync()
        self._unitTest = False

    def terminate(self):
        # Uninstall the text-to-web sync only if it was installed in the first
        # place (it depends on TRE).
        if findApproxTextInTarget:
            self._cursorMovementTimer.stop()
            # Shut down the background sync. If a sync was already in progress,
            # then discard its output.
            self._runLatest.future.cancel(True)
            self._runLatest.terminate()
            # End all callbacks.
            self._callbackManager.skipAllCallbacks()
            self._callbackManager.waitForAllCallbacks()
            # Bug: DON'T de-register the QWebChannel. This casues the error message ``onmessage is not a callable property of qt.webChannelTransport. Some things might not work as expected.`` to be displayed. I see this in https://code.woboq.org/qt5/qtwebengine/src/core/renderer/web_channel_ipc_transport.cpp.html#221; I assume it's a result of attempting to send a signal to the web page, where the web page doesn't have qwebchannel.js running.
            #self.channel.deregisterObject(self)
            # Delete it. Note that the channel is `NOT owned by the page <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`.
            sip.delete(self.channel)
            # Disconnect all signals.
            sip.delete(self)
    #
    # Vertical synchronization
    ##========================
    # These routines perform vertical synchronization.
    #
    # This function computes the distance, in pixels, measured from the target
    # cursor location to the source cursor location, as shown in part (a) of the
    # figure below: delta = source - target, so that source = target + delta.
    # This distance is limited by a constraint: the resulting target cursor
    # location must be kept a padding pixels amount away from the boundaries of
    # the target widget. Part (b) of the figure shows show this distance is
    # limited when the source lies above the target widget; the same constraint
    # applies when the source lies below the target widget.
    #
    # .. image:: sync_delta.png
    #
    # Ideally, this would instead operate on the baseline of the text, rather
    # than the bottom, but getting this is harder.
    def _alignScrollAmount(self,
      # The top (y) coordinate of the source widget in a global coordinate frame,
      # such as screen coordinates. In pixels.
      sourceGlobalTop,
      # The bottom coordinate of the cursor in the source widget, measured from the
      # top of the widget, NOT the top of the viewport. In pixels.
      sourceCursorBottom,

      # The top (y) coordinate of the target widget in a global coordinate frame,
      # such as screen coordinates. In pixels.
      targetGlobalTop,
      # The bottom coordinate of the cursor in the target widget, measured from the
      # top of the widget, NOT the top of the viewport. In pixels.
      targetCursorBottom,
      # The height of the target widget. In pixels.
      targetHeight,
      # The height of the cursor in the target widget. In pixels.
      targetCursorHeight,
      # The minimum allowable distance between target + delta and the top or
      # bottom of the target widget.
      padding):

        # Compute the raw delta between the source and target widgets.
        #
        # .. image:: dtop_initial_diagram.png
        delta = (
          # Global coords of the source cursor top.
          (sourceGlobalTop + sourceCursorBottom) -
          # Global coords of the target cursor top. The difference
          # gives the number of pixels separating them.
          (targetGlobalTop + targetCursorBottom) );

        # Constrain the resulting delta so that the stays padding pixels from
        # the top of the target widget.
        delta = max(-targetCursorBottom + targetCursorHeight + padding, delta)
        # Likewise, constrain the bottom.
        delta = min(targetHeight - targetCursorBottom - padding, delta)

        return delta

    # This string contains JavaScript code to determine the coordinates and height of the
    # anchor of the selection in the web view.
    _jsWebCursorCoords = (
        # This function returns the [top, left] position in pixels of ``obj``
        # relative to the screen, not to the viewport. This introduces one
        # potential problem: if obj is not visible when this is called, it
        # returns coordinates outside the screen (such that top or left is
        # negative or greater than the screen's height or width.
        #
        # It was slightly modified from http://www.quirksmode.org/js/findpos.html,
        #  which reproduces jQuery's offset method (https://api.jquery.com/offset/).
        'function findPos(obj) {'
            'var curLeft = 0;'
            'var curTop = 0;'
             # element.offsetLeft and element.offsetTop measure relative to
             # the object's parent. Walk the tree of parents, summing each
             # offset to determine the offset from the origin of the web page.
            'do {'
                'curLeft += obj.offsetLeft;'
                'curTop += obj.offsetTop;'
            '} while (obj = obj.offsetParent);'
            # See `element.getBoundingClientRect
            # <https://developer.mozilla.org/en-US/docs/Web/API/element.getBoundingClientRect>`_
            # for converting viewport coords to screen coords.
            'return [curLeft - window.scrollX, curTop - window.scrollY];'
        '}' +

        # This function returns [top, left, width], of the current
        # selection, where:
        #
        #   top, left - coordinates of the anchor of the
        #     selection relative to the screen, in pixels.
        #
        #   height - height at the beginning of the selection, in pixels.
        #
        # Adapted from http://stackoverflow.com/questions/2031518/javascript-selection-range-coordinates.
        # Changes:
        #
        # - jQuery usage eliminated for all but debug prints.
        # - The original code used ``range.endOffset`` instead of
        #   ``selection.focusOffset``. This caused occasional errors when
        #   dragging selections.
        'function selectionAnchorCoords() {'
            # Using ``window.getSelection()``
            # Make sure a `selection <https://developer.mozilla.org/en-US/docs/Web/API/Selection>`_ exists.
            'var selection = window.getSelection();'
            'if (selection.rangeCount == 0) return 0;'

            # The selection can contain not just a point (from a
            # single mouse click) but a range (from a mouse drag or
            # shift+arrow keys).
            # We're looking for the coordinates of the focus node
            # (the place where the mouse ends up after making the selection).
            # However, the range returned by ``selection.getRangeAt(0)``
            # begins earlier in the document and ends later, regardless
            # how the mouse was dragged. So, create a new range containing
            # just the point at the focus node, so we actually get
            # a range pointing to where the mouse is.
            # Ref: `focus <https://developer.mozilla.org/en-US/docs/Web/API/Selection.focusNode>`_ of the selection.
            # `Range <https://developer.mozilla.org/en-US/docs/Web/API/range>`_
            'var rangeAtFocus = document.createRange();'
            'rangeAtFocus.setStart(selection.focusNode, selection.focusOffset);'

            # Insert a measurable element (a span) at the selection's
            # focus.
            'var span = document.createElement("span");'
            'rangeAtFocus.insertNode(span);'

            # Measure coordinates at this span, then remove it.
            'var [left, top] = findPos(span);'
            'var height = span.offsetHeight;'
            'span.remove();'

            'return [left, top, height];'
        '}'

        # Clear the current selection, if it exists.
        'function clearSelection() {'
            'if (window.getSelection()) {'
                'window.getSelection().empty();'
            '}'
        '}'

        # Given text to find, place a highlight on the last line containing the
        # text.
        'function highlightFind('
          # The text to find, typically consisting of all text in the web page
          # from its beginning to the point to be found.
          'txt) {'

            # Clear the current selection, so that a find will start at the
            # beginning of the page.
            'clearSelection();'
            # Find or create a ``div`` used as a highlighter.
            'var highlighter = getHighlight();'
            'if (!highlighter) {'
                'highlighter = document.createElement("div");'
                'document.body.appendChild(highlighter);'
                'highlighter.style.zIndex = 100;'
                'highlighter.style.width = "100%";'
                'highlighter.style.position = "absolute";'
                # Pass any click on the highlight on to the webpage underneath.
                # See https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events.
                'highlighter.style.pointerEvents = "none";'
                'highlighter.style.backgroundColor = "rgba(255, 255, 0, 0.4)";'
                'highlighter.id = "highlighter";'
            '}'
            # See https://developer.mozilla.org/en-US/docs/Web/API/Window/find.
            ##                       aString, aCaseSensitive, aBackwards, aWrapAround, aWholeWord, aSearchInFrames, aShowDialog)
            'var found = window.find(txt,     true,           false,     false,        false,      true,            false);'
            # If the text was found, or the search string was empty, highlight a line.
            'if (found || txt === "") {'
                # Determine the coordiantes of the end of the selection.
                'var res = selectionAnchorCoords();'
                'if (res) {'
                    # Unpack the coordinates obtained.
                    'var [left, top, height] = res;'
                    # Position it based on the coordinates.
                    'highlighter.style.height = height + "px";'
                    'highlighter.style.top = (window.scrollY + top) + "px";'
                '}'
                'return true;'
            '}'
            # Remove the highlight if we can't find the text.
            'clearHighlight();'
            # Clear the selection, since we won't use it later.
            'clearSelection();'
            'return false;'
        '}'

        # Return the ``div`` used to produce a highlight, or None if it doesn't exist.
        'function getHighlight() {'
            'return document.getElementById("highlighter");'
        '}'

        # Delete the element used to produce a highlight.
        'function clearHighlight() {'
            'var highlighter = getHighlight();'
            'if (highlighter) {'
                'highlighter.remove();'
            '}'
        '}')

    # Scroll the web view to align its cursor with the qutepart cursor or vice
    # versa.
    def _scrollSync(self,
      # None to scroll the text view to the y coordinate of the web view's
      # cursor. True or False to do the opposite: scroll the web view so that
      # its cursor aligns vertically with the y coordinate of the text view. In
      # this case, True will use the tolerance to scroll only if the amount to
      # scroll exceeds that tolerance; False will scroll irregardless of the
      # tolerance.
      alreadyScrolling=None,
      # Ignored if ``alreadyScrolling == None``. Used as both a padding value and a
      # scroll tolerance, as described in alreadyScrolling.
      tolerance=50):

        # Per the `window geometry
        # <http://qt-project.org/doc/qt-4.8/application-windows.html#window-geometry>`_,
        # `geometry() <http://qt-project.org/doc/qt-4.8/qwidget.html#geometry-prop>`_
        # is relative to the parent frame. Then, use `mapToGlobal
        # <http://qt-project.org/doc/qt-4.8/qwidget.html#mapToGlobal>`_ to
        # put this in global coordinates. This works for `QWebEngineView
        # <http://doc.qt.io/qt-5/qwebengineview.html>`_, since it
        # inherits from QWidget.
        wv = self._dock._widget.webEngineView
        qp = core.workspace().currentDocument().qutepart
        qpGlobalTop = qp.mapToGlobal(qp.geometry().topLeft()).y()
        wvGlobalTop = wv.mapToGlobal(wv.geometry().topLeft()).y()

        # `qutepart.cursorRect()
        # <http://qt-project.org/doc/qt-4.8/qplaintextedit.html#cursorRect-2>`_
        # gives a value in viewport == widget coordinates. Use that directly.
        cr = qp.cursorRect()
        qpCursorHeight = cr.height()
        qpCursorBottom = cr.top() + qpCursorHeight

        # Widget height includes the scrollbars. Subtract that off to get a
        # viewable height for qutepart.
        qpHeight = qp.geometry().height()
        hsb = qp.horizontalScrollBar()
        # The scrollbar height is a constant, even if it's hidden. So, only
        # include it in calculations if it's visible.
        if hsb.isVisible():
            qpHeight -= qp.horizontalScrollBar().height()
        page = wv.page()
        wvHeight = wv.geometry().height()

        # JavaScript callback to determine the coordinates and height of the
        # anchor of the selection in the web view. It expects a 3-element tuple
        # of (left, top, height), or None if there was no selection, where:
        # top is the coordinate (in pixels) of the top of the selection, measured from the web page's origin;
        # left is the coordinate (in pixels) of the left of the selection, measured from the web page's origin.
        def callback(res):
            # See if a 3-element tuple is returned. Exit if the selection was empty.
            if not res:
                return

            _, wvCursorTop, wvCursorHeight = res
            wvCursorBottom = wvCursorTop + wvCursorHeight

            if alreadyScrolling is not None:
                deltaY = self._alignScrollAmount(qpGlobalTop, qpCursorBottom,
                  wvGlobalTop, wvCursorBottom, wvHeight, wvCursorHeight, tolerance)
                # Uncomment for helpful debug info.
                ##print(("qpGlobalTop = %d, qpCursorBottom = %d, qpHeight = %d, deltaY = %d, tol = %d\n" +
                ##  "  wvGlobalTop = %d, wvCursorBottom = %d, wvHeight = %d, wvCursorHeight = %d") %
                ##  (qpGlobalTop, qpCursorBottom, qpHeight, deltaY, tolerance,
                ##  wvGlobalTop, wvCursorBottom, wvHeight, wvCursorHeight))

                # Only scroll if we've outside the tolerance.
                if alreadyScrolling or (abs(deltaY) > tolerance):
                    # Note that scroll bars are backwards: to make the text go up, you must
                    # move the bars down (a positive delta) and vice versa. Hence, the
                    # subtration, rather than addition, below.
                    page.runJavaScript('window.scrollTo(0, window.scrollY - {});'.format(deltaY))
                # Clear the selection, whether we scrolled or not.
                self.clearSelection()
            else:
                deltaY = self._alignScrollAmount(wvGlobalTop, wvCursorBottom,
                  qpGlobalTop, qpCursorBottom, qpHeight, qpCursorHeight, 0)
                vsb = qp.verticalScrollBar()
                # The units for the vertical scroll bar is pixels, not lines. So, do
                # a kludgy conversion by assuming that all line heights are the
                # same.
                vsb.setValue(vsb.value() - round(deltaY/qpCursorHeight))

        self._dock._afterLoaded.afterLoaded(lambda: page.runJavaScript('selectionAnchorCoords();', QWebEngineScript.ApplicationWorld, self._callbackManager.callback(callback)))

    # Clear the current selection in the web view.
    def clearSelection(self):
        if not self._unitTest:
            self._dock._afterLoaded.afterLoaded(self._dock._widget.webEngineView.page().runJavaScript, 'clearSelection();', QWebEngineScript.ApplicationWorld)
    #
    #
    # Synchronizing between the text pane and the preview pane
    ##========================================================
    # A single click in the preview pane should move the text pane's cursor to the
    # corresponding location. Likewise, movement of the text pane's cursor should
    # select the corresponding text in the preview pane. To do so, an approximate
    # search for text surrounding the current cursor or click location perfomed on
    # text in the other pane provides the corresponding location in the other pane
    # to highlight.
    #
    # Bugs / to-do items
    ##------------------
    # #. I call ``toPlainText()`` several times. In the past, this was quite slow
    #    in a ``QTextEdit``. Check performance and possibly cache this value; it
    #    should be easy to update by adding a few lines to _setHtml().
    #
    # Preview-to-text sync
    ##--------------------
    # This functionaliy relies heavily on the Web to Qt bridge. Some helpful
    # references:
    #
    # * `The QtWebKit Bridge <http://qt-project.org/doc/qt-4.8/qtwebkit-bridge.html>`_
    #   gives a helpful overview.
    # * `QWebEngineView`_ is the top-level widget used to embed a Web page in a Qt
    #   application.
    #
    # For this sync, the first step is to find the single click's location in a
    # plain text rendering of the preview's web content. This is implemented in
    # JavaScript, which emits a Qt signal with the location on a click. A slot
    # connected to this signal then performs the approximate match and updates the
    # text pane's cursor. To do this:
    #
    # #. ``jsClick``, a PyQt signal with a single numeric argument (the index into
    #    a string containing the plain text rendering of the web page) is defined.
    #    This signal is `connected <onJavaScriptCleared.connect>`_ to the
    #    ``onWebviewClick`` slot.
    # #. The ``onJavaScriptCleared`` method inserts the JavaScript to listen for a
    #    click and then emit a signal giving the click's location.
    # #. The ``onWebviewClick`` method then performs the approximate match and
    #    updates the text pane's cursor location.
    # #. When a new web page is loaded, all JavaScript is lost and must be reinserted.
    #    The ``onJavaScriptCleared`` slot, connected to the
    #    ``javaScriptWindowObjectCleared`` signal, does this.
    #
    # The job of this JavaScript handler is to
    # translate a mouse click into an index into the text rendering of the
    # webpage. To do this, we must:
    #
    # #. Get the current selection made by the mouse click, which is typically
    #    an empty range. (I assume a click and drag will produce a non-empty
    #    range; however this code still works).
    # #. Extend a copy of this range so that it begins at the start of the
    #    webpage and, of course, ends at the character nearest the mouse
    #    click.
    # #. Get a string rendering of this range.
    # #. Emit a signal with the length of this string.
    #
    # Note: A JavaScript development environment with this code is available
    # at http://jsfiddle.net/hgDwx/110/.
    _jsOnClick = (

        # The `window.onclick
        # <https://developer.mozilla.org/en-US/docs/Web/API/Window.onclick>`_
        # event is "called when the user clicks the mouse button while the
        # cursor is in the window." Although the docs claim that "this event
        # is fired for any mouse button pressed", I found experimentally
        # that it on fires on a left-click release; middle and right clicks
        # had no effect.
        'function window_onclick() {'

            # Clear the current highlight -- it doesn't make sense to have other
            # text highlighted after a click.
            'clearHighlight();'

             # This performs step 1 above. In particular:
             #
             # - `window.getSelection <https://developer.mozilla.org/en-US/docs/Web/API/Window.getSelection>`_
             #   "returns a `Selection
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Selection>`_
             #   object representing the range of text selected by the
             #   user." Since this is only called after a click, I assume
             #   the Selection object is non-null.
             # - The Selection.\ `getRangeAt <https://developer.mozilla.org/en-US/docs/Web/API/Selection.getRangeAt>`_
             #   method "returns a range object representing one of the
             #   ranges currently selected." Per the Selection `glossary
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary>`_,
             #   "A user will normally only select a single range at a
             #   time..." The index for retrieving a single-selection range
             #   is of course 0.
             # - "The `Range <https://developer.mozilla.org/en-US/docs/Web/API/range>`_
             #   interface represents a fragment of a document that can
             #   contain nodes and parts of text nodes in a given document."
             #   We clone it to avoid modifying the user's existing
             #   selection using `cloneRange
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Range.cloneRange>`_.
            'var r = window.getSelection().getRangeAt(0).cloneRange();'

             # This performs step 2 above: the cloned range is now changed
             # to contain the web page from its beginning to the point where
             # the user clicked by calling `setStartBefore
             # <https://developer.mozilla.org/en-US/docs/Web/API/Range.setStartBefore>`_
             # on `document.body
             # <https://developer.mozilla.org/en-US/docs/Web/API/document.body>`_.
            'r.setStartBefore(document.body);'

             # Step 3:
             #
             # - `cloneContents <https://developer.mozilla.org/en-US/docs/Web/API/Range.cloneContents>`_
             #   "Returns a `DocumentFragment
             #   <https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment>`_
             #   copying the nodes of a Range."
             # - DocumentFragment's parent `Node <https://developer.mozilla.org/en-US/docs/Web/API/Node>`_
             #   provides a `textContent
             #   <https://developer.mozilla.org/en-US/docs/Web/API/Node.textContent>`_
             #   property which gives "a DOMString representing the textual
             #   content of an element and all its descendants." This therefore
             #   contains a text rendering of the webpage from the beginning of the
             #   page to the point where the user clicked.
             'var rStr = r.cloneContents().textContent.toString();'


             # Step 4: the length of the string gives the index of the click
             # into a string containing a text rendering of the webpage.
             # Call Python with the document's text and that index.
            'window.previewSync._onWebviewClick(document.body.textContent.toString(), rStr.length);'
        '}')

    _qtJsInit = (
        # _`Bug 1`: I can't seem to avoid the error message ``js: Uncaught TypeError: channel.execCallbacks[message.id] is not a function``. It seems like this occurs  when a new page is loaded, but the QWebChannel on the Python side sends a message intended for a previously-loaded page. Adding delays / waiting until all JS init finishes / wiating until the event queue is empty helps, but doesn't fix it. Even with these, enough busyness (constant CPU use), this still happends -- perhaps the Python/Qt network backend doesn't send these messages until the CPU is idle? As a workaround, don't define the channel until is needed, making it less likely this will happen.
        #
        # _`Bug 2`: Since ``qt`` may not be defined (Qt 5.7.0 doesn't provide the
        # ``qt`` object to JavaScript when loading per https://bugreports.qt.io/browse/QTBUG-53411),
        # wrap it in a try/except block.
        'function init_qwebchannel() {'
            # Switch event listeners, part 1/2 -- now that this init is done, don't call it again.
            'window.removeEventListener("click", init_qwebchannel);'
            'try {'
                'new QWebChannel(qt.webChannelTransport, function(channel) {'
                    # Save a reference to the previewSync object.
                    'window.previewSync = channel.objects.previewSync;'
                    # Switch event listeners, part 2/2 -- Invoke the usual onclick handler. This will only be run if the QWebChannel init succeeds.
                    'window.addEventListener("click", window_onclick);'
                    # Now that the QWebChannel is ready, use it to handle the click.
                    'window_onclick();'
                '});'
            '} catch (err) {'
                # Re-throw unrecognized errors. When ``qt`` isn't defined,
                # JavaScript reports ``js: Uncaught ReferenceError: qt is not
                # defined``; this works around `bug 2`_.
                'throw err;' #if (!(err instanceof ReferenceError)) throw err;'
            '}'
        '}'
        # Set up the sync system after a click. This works around `bug 1`_. See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener.
        'window.addEventListener("click", init_qwebchannel);'
    )

    def _initPreviewToTextSync(self):
        """Initialize the system per items 1, 2, and 4 above."""
        # When a web page finishes loading, reinsert our JavaScript.
        page = self._dock._widget.webEngineView.page()

        # Insert our scripts into every loaded page.
        qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
        if not qwebchannel_js.open(QIODevice.ReadOnly):
            raise SystemExit(
                'Failed to load qwebchannel.js with error: %s' %
                qwebchannel_js.errorString())
        qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')

        # Set up the QWebChannel. See http://doc.qt.io/qt-5/qtwebchannel-javascript.html.
        # Run the script containing QWebChannel.js first.
        beforeScript = QWebEngineScript()
        beforeScript.setSourceCode(qwebchannel_js + self._jsPreviewSync + self._qtJsInit)
        beforeScript.setName('qwebchannel.js, previewSync')
        # Run this JavaScript separated from any JavaScript present in the loaded web page. This provides better security (rogue pages can't access the QWebChannel) and better isolation (handlers, etc. won't conflict, I hope).
        beforeScript.setWorldId(QWebEngineScript.ApplicationWorld)
        beforeScript.setInjectionPoint(QWebEngineScript.DocumentCreation)
        # Per `setWebChannel <http://doc.qt.io/qt-5/qwebenginepage.html#setWebChannel>`_, only one channel is allowed per page. So, don't run this on sub-frames, since it will attempt the creation of more channels for each subframe.
        beforeScript.setRunsOnSubFrames(False)
        page.scripts().insert(beforeScript)

        # Set up the web channel. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html
        # and http://stackoverflow.com/questions/28565254/how-to-use-qt-webengine-and-qwebchannel.
        # For debug, ``set QTWEBENGINE_REMOTE_DEBUGGING=port`` then browse to
        # http://127.0.0.1:port, where port=60000 works for me. See https://riverbankcomputing.com/pipermail/pyqt/2015-August/036346.html.
        self.channel = QWebChannel(page)
        self.channel.registerObject("previewSync", self)
        # Expose the ``qt.webChannelTransport`` object in the world where these scripts live.
        page.setWebChannel(self.channel, QWebEngineScript.ApplicationWorld)

    @pyqtSlot(str, int)
    def _onWebviewClick(self, tc, webIndex):
        self._onWebviewClick_(tc, webIndex)
        # Get the qutepart text.
        qp = core.workspace().currentDocument().qutepart
        # Perform an approximate match between the clicked webpage text and the
        # qutepart text.
        textIndex = findApproxTextInTarget(tc, webIndex, qp.text)
        # Move the cursor to textIndex in qutepart, assuming corresponding text
        # was found.
        if textIndex >= 0:
            self._moveTextPaneToIndex(textIndex)

    # Used for testing -- this will be replaced by a mock. Does nothing.
    def _onWebviewClick_(self, tc, webIndex):
        pass

    def _moveTextPaneToIndex(self, textIndex, noWebSync=True):
        """Given an index into the text pane, move the cursor to that index.

        Params:

        - textIndex - The index into the text pane at which to place the cursor.
        - noWebSync - True to prevent the web-to-text sync from running as a
          result of calling this routine.
        """
        # Move the cursor to textIndex.
        qp = core.workspace().currentDocument().qutepart
        cursor = qp.textCursor()
        # Tell the text to preview sync to ignore this cursor position change.
        cursor.setPosition(textIndex, QtGui.QTextCursor.MoveAnchor)
        self._previewToTextSyncRunning = noWebSync
        qp.setTextCursor(cursor)
        self._previewToTextSyncRunning = False
        # Scroll the document to make sure the cursor is visible.
        qp.ensureCursorVisible()
        # Sync the cursors.
        self._scrollSync()
        # Focus on the editor so the cursor will be shown and ready for typing.
        core.workspace().focusCurrentDocument()

    # Text-to-preview sync
    ##--------------------
    # The opposite direction is easier, since all the work can be done in Python.
    # When the cursor moves in the text pane, find its matching location in the
    # preview pane using an approximate match. Select several characters before and
    # after the matching point to make the location more visible, since the preview
    # pane lacks a cursor. Specifically:
    #
    # #. initTextToPreviewSync sets up a timer and connects the _onCursorPositionChanged method.
    # #. _onCursorPositionChanged is called each time the cursor moves. It starts or
    #    resets a short timer. The timer's expiration calls syncTextToWeb.
    # #. syncTextToWeb performs the approximate match, then calls moveWebPaneToIndex
    #    to sync the web pane with the text pane.
    # #. moveWebToPane uses QWebFrame.find to search for the text under the anchor
    #    then select (or highlight) it.

    def _initTextToPreviewSync(self):
        """Called when constructing the PreviewDoc. It performs item 1 above."""
        # Create a timer which will sync the preview with the text cursor a
        # short time after cursor movement stops.
        self._cursorMovementTimer = QTimer()
        self._cursorMovementTimer.setInterval(300)
        self._cursorMovementTimer.timeout.connect(self.syncTextToPreview)
        # Restart this timer every time the cursor moves.
        core.workspace().cursorPositionChanged.connect(self._onCursorPositionChanged)
        # Set up a variable to tell us when the preview to text sync just fired,
        # disabling this sync. Otherwise, that sync would trigger this sync,
        # which is unnecessary.
        self._previewToTextSyncRunning = False
        # Run the approximate match in a separate thread. Cancel it if the
        # document changes.
        self._runLatest = RunLatest('QThread', self)
        self._runLatest.ac.defaultPriority = QThread.LowPriority
        core.workspace().currentDocumentChanged.connect(self._onDocumentChanged)

    def _onDocumentChanged(self, old, new):
        self._runLatest.future.cancel(True)
        self._callbackManager.skipAllCallbacks()
        self._cursorMovementTimer.stop()

    def _onCursorPositionChanged(self):
        """Called when the cursor position in the text pane changes. It (re)schedules
        a text to web sync per item 2 above. Note that the signal connected to
        this slot must be updated when the current document changes, since we only
        want cursor movement notification from the active text document. This is
        handled in _onDocumentChanged.
        """
        # Ignore this callback if a preview to text sync caused it or if the
        # preview dock is closed.
        if not self._previewToTextSyncRunning and self._dock.isVisible():
            self._cursorMovementTimer.stop()
            self._cursorMovementTimer.start()

    def syncTextToPreview(self):
        """When the timer above expires, this is called to sync text to preview
        per item 3 above. It can also be called when a sync is needed (when
        switching windows, for example).
        """
        # Only run this if we TRE is installed.
        if not findApproxTextInTarget:
            return
        # Stop the timer; the next cursor movement will restart it.
        self._cursorMovementTimer.stop()
        # Get a plain text rendering of the web view. Continue execution in a callback.
        qp = core.workspace().currentDocument().qutepart
        qp_text = qp.text
        self._dock._widget.webEngineView.page().toPlainText(
            self._callbackManager.callback(self._havePlainText))

    # Perform an approximate match in a separate thread, then update
    # the cursor based on the match results.
    def _havePlainText(self, html_text):
        # Performance notes: findApproxTextInTarget is REALLY slow. Scrolling
        # through preview.py with profiling enabled produced::
        #
        #  Output from Enki:
        #         41130 function calls in 3.642 seconds
        #
        #   Ordered by: standard name
        #
        #   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        #       13    0.000    0.000    0.000    0.000 __init__.py:406(text)
        #       13    0.000    0.000    3.398    0.261 approx_match.py:138(findApproxText)
        #       13    0.000    0.000    3.432    0.264 approx_match.py:175(findApproxTextInTarget)
        #       13    0.029    0.002    0.034    0.003 approx_match.py:252(refineSearchResult)
        #       26    0.000    0.000    0.000    0.000 core.py:177(workspace)
        #       ...snip lots more 0.000 or very small times...
        #
        # Therefore, finding ways to make this faster or run it in another
        # thread should significantly improve the GUI's responsiveness.
        qp = core.workspace().currentDocument().qutepart
        qp_text = qp.text
        qp_position = qp.textCursor().position()
        self._runLatest.start(self._movePreviewPaneToIndex,
            # Call findApproxTextInTarget, returning the index and the HTML text searched.
            lambda: (findApproxTextInTarget(qp_text, qp_position, html_text), html_text))

    def _movePreviewPaneToIndex(self, future):
        """Highlights webIndex in the preview pane, per item 4 above.

        Params:

        - webIndex - The index to move the cursor / highlight to in the preview
          pane.
        - txt - The text of the webpage, returned by mainFrame.toPlainText().
        """
        # Retrieve the return value from findApproxTextInTarget.
        webIndex, txt = future.result

        view = self._dock._widget.webEngineView
        page = view.page()
        ft = txt[:webIndex]

        def callback(found):
            if found:
                # Sync the cursors.
                self._scrollSync(False)
                self.textToPreviewSynced.emit()

        if webIndex >= 0:
            self._dock._afterLoaded.afterLoaded(lambda: page.runJavaScript('highlightFind({});'.format(repr(ft)), QWebEngineScript.ApplicationWorld, self._callbackManager.callback(callback)))
        else:
            self.clearHighlight()

    def clearHighlight(self):
        self._dock._afterLoaded.afterLoaded(self._dock._widget.webEngineView.page().runJavaScript, 'clearHighlight();', QWebEngineScript.ApplicationWorld)
Beispiel #26
0
class CustomWebPage(QWebEnginePage):
    def __init__(self, parent):
        self.myprofile = self.getProfile()
        super().__init__(self.myprofile, parent)
        self.profile = self.myprofile
        self.jsReturned = False
        self.channel = None
        self._overrideFile = None
        #self.applyCustomStyleSheet()
        self.loadStarted.connect(self.slotFrameLoadStarted)
        self.urlChanged.connect(self.slotUrlChanged, Qt.QueuedConnection)
        self.loadFinished.connect(self.injectXwareDesktop)
        app.sigMainWinLoaded.connect(self.connectUI)
        self.javaScriptConsoleMessage(QWebEnginePage.InfoMessageLevel, 'Info',
                                      0, 'Error')

    sigFrameLoadStarted = pyqtSignal()

    @pyqtSlot()
    def connectUI(self):
        app.mainWin.action_refreshPage.triggered.connect(self.slotRefreshPage)

    # Local Torrent File Chooser Support
    def chooseFile(self, parentFrame, suggestFile):
        if self._overrideFile:
            return self.overrideFile
        else:
            return super().chooseFile(parentFrame, suggestFile)

    @property
    def overrideFile(self):
        result = self._overrideFile
        self._overrideFile = None
        return result

    @overrideFile.setter
    def overrideFile(self, url):
        self._overrideFile = url

    def urlMatchIn(self, *againsts):
        return parse.urldefrag(self.url().toString())[0] in againsts

    # Local Torrent File Chooser Support Ends Here

    def applyCustomStyleSheet(self):
        styleSheet = QUrl.fromLocalFile(constants.XWARESTYLE_FILE)
        self.setHtml("U", styleSheet)

    # mainFrame functions
    @pyqtSlot()
    def slotFrameLoadStarted(self):
        self.overrideFile = None
        self.sigFrameLoadStarted.emit()

    @pyqtSlot()
    def slotUrlChanged(self):
        if self.urlMatchIn(constants.V2_PAGE):
            logging.info("webView: redirect to V3.")
            self.triggerAction(QWebEnginePage.Stop)
            self.load(QUrl(constants.V3_PAGE))
        elif self.urlMatchIn(constants.V3_PAGE, constants.LOGIN_PAGE):
            pass
        else:
            logging.error("Unable to handle {}".format(self.url().toString()))

    def addobject(self):
        if app is not None:
            if self.channel is None:
                self.channel = QWebChannel()
            self.channel.registerObject('xdpy1', app.frontendpy)
            self.setWebChannel(self.channel)

    def addobject(self, name):
        if app is not None:
            if self.channel is None:
                self.channel = QWebChannel()
            self.channel.registerObject(name, app.frontendpy)
            self.setWebChannel(self.channel)

    @pyqtSlot()
    def blockUpdates(self):
        print('blockUpdates')

    def getProfile(self):

        path = constants.QWEBENGINECACHE_PATH
        tryMkdir(path)
        profile = QWebEngineProfile()
        profile.setCachePath(path)
        profile.clearHttpCache()
        jsFile = constants.QTWEBCHANNELJS_FILE
        with open(jsFile, encoding="UTF-8") as file:
            js = file.read()
        script = QWebEngineScript()
        script.setSourceCode(js)
        script.setName('qwebchannel.js')
        script.setInjectionPoint(QWebEngineScript.DocumentCreation)
        script.setWorldId(QWebEngineScript.MainWorld)
        script.setRunsOnSubFrames(False)
        profile.scripts().insert(script)
        return profile

    @pyqtSlot("QList<QVariant>")
    def js_callback(self, result):
        if result is not None:
            callBackResult = result
            print("js_callback:{}".format(result))
        self.jsReturned = True

    def injectJs(self, source, name):
        js = QWebEngineScript()
        js.setName(name)
        js.setSourceCode(source)
        js.setInjectionPoint(QWebEngineScript.DocumentCreation)
        js.setWorldId(QWebEngineScript.MainWorld)
        js.setRunsOnSubFrames(True)
        self.scripts().insert(js)

    def getUrl(self):
        if self.urlMatchIn(constants.LOGIN_PAGE):
            jsCode = 'document.getElementsByTagName("iFrame").item(0).src'
            self.runJavaScript(jsCode, self.js_callback)
        elif self.urlMatchIn(constants.V3_PAGE):
            print('False!')

    @pyqtSlot(bool)
    def injectXwareDesktop(self, ok):
        if not ok:  # terminated prematurely
            return
        js_prifx = '\nnew QWebChannel(qt.webChannelTransport, function(channel){window.xdpy=channel.objects.xdpy;\n'
        js_suffix = '\n})\n'

        if self.urlMatchIn(constants.LOGIN_PAGE):
            jsFile = constants.XWAREJS_LOGIN_FILE
        else:
            jsFile = constants.XWAREJS_FILE

        self.addobject('xdpy')
        with open(jsFile, encoding="UTF-8") as file:
            js = file.read()
        js_main = js_prifx + js + js_suffix
        self.runJavaScript(js_main, self.js_callback)

    @pyqtSlot()
    def slotRefreshPage(self):
        self.load(QUrl(constants.V3_PAGE))
 def register_channel_objects():
     wc = QWebChannel(page)
     wc.registerObjects(wcobjects)
     page.setWebChannel(wc)
Beispiel #28
0
        self.setGeometry(300, 300, 300, 220)
        self.show()

    def s1s(self):
        self.handler._signal1.emit("b1")

    def s2s(self):
        self.handler._signal2.emit("b2")


if __name__ == '__main__':
    app = QApplication(sys.argv)

    s = QWidget()
    view = QWebEngineView(s)
    channel = QWebChannel(view)
    handler = CallHandler()
    channel.registerObject('pyjs', handler)
    handler._signal1.connect(handler._slot1)

    view.page().setWebChannel(channel)

    b1 = QPushButton("b1", s)
    #b1.clicked.connect()

    url_string = "file:///test.html"
    view.load(QUrl(url_string))
    s.setGeometry(300, 300, 300, 220)

    s.show()
    sys.exit(app.exec_())
        self.site.setStyleSheet("height:30px")
        self.toolbar.addWidget(self.site)
        self.refresh = QAction(QIcon("../images/refresh.png"), "刷新", self)
        self.toolbar.addAction(self.refresh)
        self.toolbar.actionTriggered[QAction].connect(
            lambda: MainWebView(self).refresh(self.site.text()))
        self.toolbar.addSeparator()

    def resizeEvent(self, event):
        if self.isResize:
            height = self.size().height()
            width = self.size().width()
            self.webview.setMinimumSize(width, height)
            self.webview.setMaximumSize(width, height)
        else:
            self.isResize = True


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Ui_Main_WebView()
    main.show()

    main.webview.load(
        QUrl("http://www.bidcenter.com.cn/newssearchyz-19852799.html"))
    channel = QWebChannel()
    myObj = PageToQt()
    channel.registerObject('bridge', myObj)
    main.webview.page().setWebChannel(channel)

    sys.exit(app.exec_())
Beispiel #30
0
    def __init__(self, qMainWindow=None, qtDesigner=None):
        if self.__class__.logger is None:
            self.__class__.logger = logging.getLogger('root')

        if self.__class__.loader is None:
            self.__class__.loader = genshi.template.TemplateLoader(
                os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 'www')),
                auto_reload=True
            )

        if self.__class__.qMainWindow is None and qMainWindow is not None:
            self.__class__.qMainWindow = qMainWindow

        if self.__class__.qtDesigner is None and qtDesigner is not None:
            self.__class__.qtDesigner = qtDesigner

            class WebEnginePage(QWebEnginePage):
                updated = pyqtSignal(str, float, str)

                def javaScriptConsoleMessage(self, level, msg, line, source):
                    harkfm.Interface.logger.debug('%s:%s  %s', source, line, msg)

                def acceptNavigationRequest(self, url, type, is_main_frame):
                    if type == QWebEnginePage.NavigationTypeLinkClicked:
                        webbrowser.open(url.toString())
                        return False
                    return True

                # Turn a jQuery.serialize() query into a dict
                def _deserialize(self, query):
                    form = {}
                    query = query.split('&')
                    for val in query:
                        val = val.split('=')
                        form[urllib.parse.unquote(val[0])] = urllib.parse.unquote(val[1])
                    return form

                @pyqtSlot()
                def update(self):
                    if engine.current is not None:
                        self.updated.emit(engine.current.elapsed, engine.current.percent, engine.current.remaining)

                @pyqtSlot()
                def queued(self):
                    queued = 0
                    if engine.current is not None:
                        queued = engine.current.queued
                    self.updated.emit(sys._getframe().f_code.co_name, queued)

                @pyqtSlot()
                def love(self):
                    engine.current.love()
                    interface = harkfm.Interface()
                    interface.index()

                @pyqtSlot()
                def unlove(self):
                    engine.current.unlove()
                    interface = harkfm.Interface()
                    interface.index()

                @pyqtSlot(str)
                def login(self, query):
                    form = self._deserialize(query)
                    storage = harkfm.Storage()
                    api_key = harkfm.Engine.config['apis']['last.fm']['key']
                    api_secret = harkfm.Engine.config['apis']['last.fm']['secret']
                    try:
                        network = pylast.get_lastfm_network(api_key, api_secret)
                        session_key = pylast.SessionKeyGenerator(network).get_session_key(form['username'],  pylast.md5(form['password']))
                        storage.config_set('apis/last.fm/session_key', session_key)
                        interface = harkfm.Interface()
                        interface.index()
                    except Exception as e:
                        harkfm.Interface.logger.error('%s  %s', type(e), e)

                @pyqtSlot()
                def logout(self):
                    engine = harkfm.Engine()
                    engine.lfm_logout()

                @pyqtSlot(str)
                def save_settings(self, form):
                    form = json.loads(form)
                    storage = harkfm.Storage()
                    for key in form:
                        if type(form[key]) is str and form[key].isdigit():
                            form[key] = int(form[key])
                        storage.config_set('settings/' + key, form[key])
                    interface = harkfm.Interface()
                    interface.index()

            # Set custom page
            page = WebEnginePage(self.QWebEngineView)
            page.profile().setHttpCacheType(QWebEngineProfile.NoCache)
            self.QWebEngineView.setPage(page)

            # Set custom channel
            channel = QWebChannel(page)
            channel.registerObject('py', page)
            page.setWebChannel(channel)

            engine = harkfm.Engine()  # init harkfm.Engine
            lfm_network = engine.lfm_login()
            if lfm_network is None:
                self.login()
            else:
                self.index()
Beispiel #31
0
class WebView(object):
    def __init__(self, parent, object_name, dev_mode: bool):
        self._event_listeners = {}
        self._web_engine_view = None
        self._dev_mode = dev_mode
        self._object_name = object_name
        self._setup_web_engine_view(parent)
        self.move_window_func = None

    def _setup_web_engine_view(self, parent) -> NoReturn:
        self._web_engine_view = self._create_web_engine_view(
            parent, self._object_name)
        self._web_engine_view.urlChanged.connect(self._on_url_changed)
        self._web_engine_view.page().loadFinished.connect(
            self._on_page_load_finished)

        # cookies
        self._cookiesJar = CookiesJar()
        self._web_engine_view.page().profile().cookieStore(
        ).cookieAdded.connect(self._on_cookie_added)
        self._web_engine_view.page().profile().cookieStore(
        ).cookieRemoved.connect(self._on_cookie_removed)

        # web channel
        self._web_channel = QWebChannel(self._web_engine_view.page())
        self._web_engine_view.page().setWebChannel(self._web_channel)
        self._web_bridge = WebViewBridge(self)
        self._web_channel.registerObject("proxy", self._web_bridge)

        # inject scripts
        self.inject_javascript_file(
            os.path.join(os.path.dirname(os.path.abspath(__file__)), "res",
                         "js", "qwebchannel.js"))
        self.inject_javascript_file(
            os.path.join(os.path.dirname(os.path.abspath(__file__)), "res",
                         "js", "mmgui.js"))

    def register_web_channel_object(self, namespace: str,
                                    object: QObject) -> NoReturn:
        self._web_channel.registerObject(namespace, object)

    def get_web_engine_view(self):
        return self._web_engine_view

    def get_web_engine_view_page(self):
        return self._web_engine_view.page()

    def get_widget_view(self):
        return self._web_engine_view

    def register_event_listener(
            self, event_type: str, listener: Callable[[WebViewEvent],
                                                      None]) -> NoReturn:
        if event_type not in self._event_listeners:
            self._event_listeners[event_type] = []
        self._event_listeners[event_type].append(listener)

    def unregister_event_listener(
            self, event_type: str, listener: Callable[[WebViewEvent],
                                                      None]) -> NoReturn:
        if event_type in self._event_listeners and self._event_listeners[
                event_type]:
            for item in self._event_listeners[event_type]:
                if item == listener:
                    self._event_listeners[event_type].remove(item)
                    break

    def _notify_event_listeners(self, event: WebViewEvent):
        event_type = event.type
        if event_type in self._event_listeners and self._event_listeners[
                event_type]:
            for listener in self._event_listeners[event_type]:
                listener(event)

    def _on_url_changed(self, qurl):
        logger.info("_on_url_changed %s", qurl.toString())
        self._notify_event_listeners(
            WebViewEvent("on_url_changed", qurl.toString()))

    def _on_page_load_finished(self, ok):
        logger.info("_on_page_load_finished %s", ok)
        self._notify_event_listeners(WebViewEvent("on_page_load_finished", ok))

    def _on_cookie_added(self, cookie):
        logger.info("_on_cookie_added %s", cookie)
        self._cookiesJar.add_cookie(cookie)

    def _on_cookie_removed(self, cookie):
        logger.info("_on_cookie_removed %s", cookie)
        self._cookiesJar.remove_cookie(cookie)

    def delete_all_cookies(self):
        self._web_engine_view.page().profile().cookieStore().deleteAllCookies()

    def get_cookie(self, domain, name):
        return self._cookiesJar.get_cookie(domain, name)

    def _create_web_engine_view(self, parent,
                                object_name: str) -> QWebEngineView:
        web_engine_view = MyQWebEngineView(parent, lambda e: self._on_drop(e),
                                           self._dev_mode)
        web_engine_view.setAutoFillBackground(False)
        web_engine_view.setStyleSheet("")
        web_engine_view.setUrl(QtCore.QUrl("about:blank"))
        web_engine_view.setObjectName(object_name)
        web_engine_view.setContextMenuPolicy(
            QtCore.Qt.CustomContextMenu)  # 禁用右键菜单
        return web_engine_view

    def _on_drop(self, event: QDropEvent) -> NoReturn:
        pass  # TODO

    def bind_function(self, js_function_name: str,
                      py_function: Callable) -> NoReturn:
        self._web_bridge.bind_function(js_function_name, py_function)

    def send_message_to_js(self, msg: Any) -> NoReturn:
        self._web_bridge.send_message(msg)

    def run_javascript_code(self, javascript_code: str,
                            callback: Callable[[Any], None]) -> NoReturn:
        self._web_engine_view.page().runJavaScript(javascript_code, callback)

    def inject_javascript_file(self, javascript_file: str) -> NoReturn:
        if not os.path.exists(javascript_file):
            raise Exception("javascript file is not exists, filename=%s" %
                            javascript_file)

        script = QWebEngineScript()
        with codecs.open(javascript_file, "r", "utf-8") as f:
            script.setSourceCode(f.read())
        script.setName(os.path.basename(javascript_file))
        script.setWorldId(QWebEngineScript.MainWorld)
        script.setInjectionPoint(QWebEngineScript.DocumentCreation)
        script.setRunsOnSubFrames(False)

        profile = self._web_engine_view.page().profile(
        )  # type: QWebEngineProfile
        scripts = profile.scripts()  # type: QWebEngineScriptCollection
        scripts.insert(script)
        logger.info("inject javascript file %s" % javascript_file)

    def load_file(self, filename: str) -> NoReturn:
        self._web_engine_view.load(QUrl.fromLocalFile(filename))

    def load_url(self, url: str) -> NoReturn:
        self._web_engine_view.load(QUrl(url))

    def set_web_dev_tools_page(self, page) -> NoReturn:
        self._web_engine_view.page().setDevToolsPage(page)

    def reload(self):
        self._web_engine_view.reload()

    def destroy(self):
        self._web_engine_view.urlChanged.disconnect()
        self._web_engine_view.deleteLater()
        self._web_engine_view.close()
Beispiel #32
0
# project:  PyQt5
# File : WebViewJSTest02.py 
# @software: PyCharm


import sys
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from 高级界面控件.MyShareObject import MyShareObject
from PyQt5.QtWebChannel import QWebChannel

if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = QWidget()
    win.setWindowTitle('Web 页面中 JavaScript 与 QWebEngineView 交互示例')
    win.setWindowIcon(QIcon("./images/Python2.ico"))
    layout = QVBoxLayout()
    win.setLayout(layout)
    view = QWebEngineView()
    htmlUrl = 'http://192.168.3.105:8020/web/index.html'
    view.load(QUrl(htmlUrl))
    channel = QWebChannel()
    myObj = MyShareObject()
    channel.registerObject("bridge", myObj)
    view.page().setWebChannel(channel)
    layout.addWidget(view)
    win.show()
    sys.exit(app.exec_())
class scMap(QWidget):
    # map class used
    def __init__(self, app, parent=None):
        super().__init__(parent=parent)
        self.app = app
        self.browser = QWebEngineView(self)

        self.channel = QWebChannel()
        self.bridge = Bridge()
        self.channel.registerObject('bridge', self.bridge)
        self.browser.page().setWebChannel(self.channel)

        self.initUI()

        self.bool_zoom_change = True
        self.bool_reset_map = True
        self.zoom_got = None
        self.bounds_got = None

        self.bool_contains = None
        self.bool_contains_1 = None
        self.bool_contains_2 = None

    def initUI(self):
        self.browser.load(QUrl("file:///display/map.html"))
        self.browser.setFixedSize(783, 616)
        self.show()

    def runJavaScript(self, js_str):
        self.browser.page().runJavaScript(js_str)

    def runJavaScript_with_callback(self, js_str, callback):
        self.browser.page().runJavaScript(js_str, callback)

    def createMarker(self, name, lat, lng, icon):
        js_str = """%s = new google.maps.Marker({
              position: {lat: %f, lng: %f},
              map: map,
              icon: "data/%s"
          });""" % (name, lat, lng, icon)
        self.runJavaScript(js_str)

    def moveMarker(self, name, lat, lng, link):

        # 判断是否越界,越界则重置地图中心与缩放
        if self.cross_the_border(lat, lng, link):
            # print('cross_the_border')
            self.reset_map(link)

        js_str = """%s.setPosition({lat: %f, lng: %f})""" % (name, lat, lng)
        self.runJavaScript(js_str)

    def deleteMarker(self, name):
        pass

    def setCenter(self, lat, lng):
        js_str = """map.setCenter({lat: %f, lng: %f})""" % (lat, lng)
        self.runJavaScript(js_str)

    def panTo(self, lat, lng):
        js_str = """map.panTo({lat: %f, lng: %f})""" % (lat, lng)
        self.runJavaScript(js_str)

    def set_home(self, lat, lng):
        js_str = """home = new google.maps.Marker({
                    position: {lat: %f, lng: %f},
                    map: map,
                    icon: "data/home.png"
                });""" % (lat, lng)
        self.runJavaScript(js_str)

    def cross_the_border(self, lat, lng, link):
        # None for no result

        js_str = "map.getBounds().contains({lat: %f, lng: %f})" % (lat, lng)
        self.runJavaScript_with_callback(js_str, link.js_callback)

        bool_ctb = link.js_result
        link.js_result = None
        if bool_ctb is None:
            return bool_ctb
        else:
            return not bool_ctb

    def cross_the_border_1(self, lat, lng):
        # None for no result
        js_str = "map.getBounds().contains({lat: %f, lng: %f})" % (lat, lng)
        self.runJavaScript_with_callback(js_str, self.ctb_callback_1)

        bool_ctb = self.bool_contains_1
        self.bool_contains_1 = None
        if bool_ctb is None:
            return bool_ctb
        else:
            return not bool_ctb

    def cross_the_border_2(self, lat, lng):
        # None for no result
        js_str = "map.getBounds().contains({lat: %f, lng: %f})" % (lat, lng)
        self.runJavaScript_with_callback(js_str, self.ctb_callback_2)

        bool_ctb = self.bool_contains_2
        self.bool_contains_2 = None
        if bool_ctb is None:
            return bool_ctb
        else:
            return not bool_ctb

    def ctb_callback(self, result):
        self.bool_contains = result

    def ctb_callback_1(self, result):
        self.bool_contains_1 = result

    def ctb_callback_2(self, result):
        self.bool_contains_2 = result

    def callback_print(self, v):
        print(type(v), v)

    def reset_map(self, link):
        if self.bool_reset_map:
            self.bool_reset_map = False

            lat1 = None
            lat2 = None
            lng1 = None
            lng2 = None

            for linkInt in self.app.toolbox.getLinkManager().links:
                if lat1 is None or linkInt.uav_lat < lat1:
                    lat1 = linkInt.uav_lat
                if lat2 is None or linkInt.uav_lat > lat2:
                    lat2 = linkInt.uav_lat
                if lng1 is None or linkInt.uav_lng < lng1:
                    lng1 = linkInt.uav_lng
                if lng2 is None or linkInt.uav_lng > lng2:
                    lng2 = linkInt.uav_lng

            self.panTo((lat1 + lat2) / 2, (lng1 + lng2) / 2)

            # 预留范围
            p = 1 / 8
            lat1_goal = lat1 - (lat2 - lat1) * p
            lat2_goal = lat2 + (lat2 - lat1) * p
            lng1_goal = lng1 - (lng2 - lng1) * p
            lng2_goal = lng2 + (lng2 - lng1) * p

            # get map bounds (lat1_got, lat2_got, lng1_got, lng2_got)
            bounds = None
            while bounds is None:
                bounds = self.getBounds()
            lat1_got, lat2_got, lng1_got, lng2_got = bounds
            # get zoom(n_got)
            n_got = None
            while n_got is None:
                n_got = self.getZoom()
            # compare (lat1_goal, lat2_goal, lng1_goal, lng2_goal) and (lat1_got, lat2_got, lng1_got, lng2_got)
            ratio = max(([(lat2_goal - lat1_goal) / (lat2_got - lat1_got),
                          (lng2_goal - lng1_goal) / (lng2_got - lng1_got)]))
            # confirm delta
            delta = 0
            if ratio > 1:
                # while ratio > 1:
                #    delta -= 1
                #    ratio /= 2
                delta = -1
            elif 1 / 2 >= ratio > 0:
                # while ratio <= 1 / 2:
                #   delta += 1
                #   ratio *= 2
                delta = 1
            elif ratio == 0 or 1 >= ratio > 1 / 2:
                pass
            else:
                input('reset map::ratio error')

            n_goal = n_got + delta
            if n_goal > 20:
                n_goal = 20
            elif n_goal < 3:
                n_goal = 3

            #if delta != 0:
            #    print(threading.current_thread().getName(), 'delta:', delta, '|', 'n_goal:', n_goal)
            #    print((lat1_goal, lat2_goal, lng1_goal, lng2_goal), (lat1_got, lat2_got, lng1_got, lng2_got))

            # self.zoom_change(zoom=n_goal)
            self.fitBounds(lat1_goal, lat2_goal, lng1_goal, lng2_goal)

            self.bool_reset_map = True

    def fitBounds(self, lat1_goal, lat2_goal, lng1_goal, lng2_goal):
        js_str = '''fitBounds(%f, %f, %f, %f)''' % (lat1_goal, lat2_goal,
                                                    lng1_goal, lng2_goal)
        self.runJavaScript(js_str)

    def zoom_change(self, delta=-1, zoom=None):
        if self.bool_zoom_change:
            self.bool_zoom_change = False

            if zoom is None:
                n_got = None
                while n_got is None:
                    n_got = self.getZoom()

                n_goal = n_got + delta
                if n_goal > 20:
                    n_goal = 20
                elif n_goal < 3:
                    n_goal = 3

                if delta != 0:
                    print('n_goal:', n_goal,
                          threading.current_thread().getName())

                while n_got != n_goal:
                    # setZoom
                    js_str = '''map.setZoom(%d)''' % n_goal
                    self.runJavaScript(js_str)
                    # getZoom
                    n_temp = None
                    while n_temp is None:
                        n_temp = self.getZoom()
                    n_got = n_temp
            else:
                n_got = None
                while n_got is None:
                    n_got = self.getZoom()

                n_goal = zoom
                while n_got != n_goal:
                    # setZoom
                    js_str = '''map.setZoom(%d)''' % n_goal
                    self.runJavaScript(js_str)
                    # getZoom
                    n_temp = None
                    while n_temp is None:
                        n_temp = self.getZoom()
                    n_got = n_temp

        self.bool_zoom_change = True

    def getZoom(self):
        js_str = '''map.getZoom()'''
        self.runJavaScript_with_callback(js_str, self.callback_getZoom)
        zoom = self.zoom_got
        self.zoom_got = None
        return zoom

    def callback_getZoom(self, result):
        self.zoom_got = result

    def getBounds(self):
        js_str = '''map.getBounds()'''
        self.runJavaScript_with_callback(js_str, self.callback_getBounds)
        bounds = self.bounds_got
        self.bounds_got = None
        if bounds is None:
            return bounds
        else:
            # return (lat1_got, lat2_got, lng1_got, lng2_got)
            return (bounds['f']['b'], bounds['f']['f'], bounds['b']['b'],
                    bounds['b']['f'])

    def callback_getBounds(self, result):
        self.bounds_got = result
Beispiel #34
0
    url = QUrl('file:///home/yyk/Desktop/index.html')
    mainwindow.view.page.load(url)

    # jsFile = constants.QTWEBCHANNELJS_FILE
    # with open(jsFile, encoding="UTF-8") as file:
    #     js = file.read()
    # script = QWebEngineScript();
    # script.setSourceCode(js)
    # script.setName('qwebchannel.js')
    # script.setInjectionPoint(QWebEngineScript.DocumentCreation)
    # script.setWorldId(QWebEngineScript.MainWorld)
    # script.setRunsOnSubFrames(False)
    #
    # mainwindow.view.page.profile().scripts().insert(script)

    channel = QWebChannel()
    myobject=myObject()
    channel.registerObject("xdpy",myobject)
    mainwindow.view.page.setWebChannel(channel)

    #
    # jsFile = constants.QTWEBCHANNELJS_FILE
    # with open(jsFile, encoding="UTF-8") as file:
    #     js = file.read()
    # mainwindow.injectJS(js, 'qwebchannel.js')


    js='\nnew QWebChannel(qt.webChannelTransport, function(channel){alert("正在调用回调函数");window.xdpy=channel.objects.xdpy;xdpy.test();});\n'
    #mainwindow.view.page.runJavaScript(js,mainwindow.js_callback)
    # mainwindow.view.page.runJavaScript('window.channel="AAAA"', mainwindow.js_callback)
    mainwindow.view.page.runJavaScript(js,mainwindow.js_callback)
Beispiel #35
0
        def __init__(self, root):
            super(VdomrWebView, self).__init__()

            self._root = root

            self._channel = QWebChannel()
            self._pyqt5_api = PyQt5Api()
            self._channel.registerObject('pyqt5_api', self._pyqt5_api)
            self.page().setWebChannel(self._channel)

            html = """
                <html>
                <head>
                <style>
                .overlay {
                    background-color: rgba(1, 1, 1, 0.8);
                    color:white;
                    font-size:24px;
                    bottom: 0;
                    left: 0;
                    position: fixed;
                    right: 0;
                    top: 0;
                    text-align: center;
                    padding: 40px;
                }
                </style>
                <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
                <script>
                // we do the following so we can get all console.log messages on the python console
                console.log=console.error;
                {script}
                </script>
                </head>
                <body>
                <div id=overlay class=overlay style="visibility:hidden">Please wait...</div>
                {content}
                <script language="JavaScript">
                    new QWebChannel(qt.webChannelTransport, function (channel) {
                        window.pyqt5_api = channel.objects.pyqt5_api;
                        /*
                        // instead of doing the following, we map console.log to console.error above
                        console.log=function(a,b,c) {
                            let txt;
                            if (c) txt=a+' '+b+' '+c;
                            else if (b) txt=a+' '+b;
                            else txt=a+'';
                            pyqt5_api.console_log(txt);
                        }
                        */
                    });
                </script>
                </body>
            """
            html = html.replace('{content}', root._repr_html_())

            script = """
                window.show_overlay=function() {
                    document.getElementById('overlay').style.visibility='visible'
                }
                window.hide_overlay=function() {
                    document.getElementById('overlay').style.visibility='hidden'
                }
                window.vdomr_invokeFunction=function(callback_id,args,kwargs) {
                    window.show_overlay();
                    setTimeout(function() {
                        pyqt5_api.invokeFunction({callback_id:callback_id,args:args,kwargs:kwargs});
                        window.hide_overlay();
                    },100); // the timeout might be important to prevent crashes, not sure
                }
            """
            while True:
                js = _take_javascript_to_execute()
                if js is None:
                    break
                script = script+'\n'+js
            html = html.replace('{script}', script)

            self.page().setHtml(html)
Beispiel #36
0
 def set_action(self, action, d):
     self.page().setWebChannel(None)
     channel = QWebChannel(self.page())
     channel.registerObject(action.action(), action)
     self.page().setWebChannel(channel)
     action.callback = d
Beispiel #37
0
class VQMemoryCanvas(e_memcanvas.MemoryCanvas, QWebEngineView):

    #syncSignal = QtCore.pyqtSignal()
    def __init__(self, mem, syms=None, parent=None, **kwargs):
        e_memcanvas.MemoryCanvas.__init__(self, mem=mem, syms=syms)
        QWebEngineView.__init__(self, parent=parent, **kwargs)

        self._canv_cache = None
        self._canv_curva = None
        self._canv_rendtagid = '#memcanvas'
        self._canv_rend_middle = False
        self.fname = None

        # DEV: DO NOT DELETE THIS REFERENCE TO THESE
        # Otherwise they'll get garbage collected and things like double click
        # to navigate and logging won't work
        # (but pyqt5 won't throw an exception, because ugh).
        self._log_page = LoggerPage()
        self.setPage(self._log_page)
        self.channel = QWebChannel()
        self.page().setWebChannel(self.channel)
        self.channel.registerObject('vnav', self)

        htmlpage = e_q_html.template.replace('{{hotexamples_com}}',
                                             e_q_jquery.jquery_2_1_0)
        page = self.page()
        page.setHtml(htmlpage)
        loop = QtCore.QEventLoop()
        page.loadFinished.connect(loop.quit)
        loop.exec()
        QtCore.QCoreApplication.processEvents(
            QtCore.QEventLoop.ExcludeUserInputEvents
            | QtCore.QEventLoop.ExcludeSocketNotifiers)
        page.runJavaScript(e_q_jquery.jquery_2_1_0)
        self.forceSync()

        page.contentsSizeChanged.connect(self._frameContentsSizeChanged)

        # Allow our parent to handle these...
        self.setAcceptDrops(False)

    def forceSync(self):
        cthr = QtCore.QThread.currentThread()
        loop = QtCore.QThread.eventDispatcher(cthr)
        loop.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents
                           | QtCore.QEventLoop.ExcludeSocketNotifiers
                           | QtCore.QEventLoop.WaitForMoreEvents)

    def renderMemory(self, va, size, rend=None):

        if self._canv_rend_middle:
            vmap = self.mem.getMemoryMap(va)
            if vmap is None:
                raise e_exc.InvalidAddress(va)

            origva = va
            va, szdiff = self._loc_helper(max(va - size, vmap[0]))
            size += size + szdiff

            def callScroll(data):
                self._scrollToVa(origva)

            ret = e_memcanvas.MemoryCanvas.renderMemory(self,
                                                        va,
                                                        size,
                                                        rend=rend,
                                                        cb=callScroll)
        else:
            ret = e_memcanvas.MemoryCanvas.renderMemory(self,
                                                        va,
                                                        size,
                                                        rend=rend)

        return ret

    def _frameContentsSizeChanged(self, size):
        if self._canv_scrolled:
            page = self.page()
            page.runJavaScript('window.scrollTo(1, 0x0fffffff);')

    @idlethread
    def _scrollToVa(self, va, cb=None):
        vq_main.eatevents()  # Let all render events go first
        page = self.page()
        selector = 'viv:0x%.8x' % va
        js = f'''
        var nodes = document.getElementsByName("{selector}");
        if (nodes != null && nodes.length > 0) {{
            nodes[0].scrollIntoView()
        }}
        '''
        if cb:
            page.runJavaScript(js, cb)
        else:
            page.runJavaScript(js)

    @idlethread
    def _selectVa(self, va, cb=None):
        page = self.page()
        js = '''
        selectva("0x%.8x");
        scrolltoid("a_%.8x");
        ''' % (va, va)
        page.runJavaSript(js, cb)

    def _beginRenderMemory(self, va, size, rend):
        self._canv_cache = ''

    def _endRenderMemory(self, va, size, rend, cb=None):
        self._appendInside(self._canv_cache, cb)
        self._canv_cache = None

    def _beginRenderVa(self, va, cb=None):
        self._add_raw('<a name="viv:0x%.8x" id="a_%.8x">' % (va, va), cb)

    def _endRenderVa(self, va, cb=None):
        self._add_raw('</a>', cb)

    def _beginUpdateVas(self, valist, cb=None):
        self._canv_cache = ''

        page = self.page()

        selector = 'a#a_%.8x' % valist[0][0]
        js = f'''
        node = document.querySelector("{selector}");
        node.outerHTML = '<update id="updatetmp"></update>' + node.outerHTML;
        '''

        for va, size in valist:
            selector = 'a#a_%.8x' % va
            js += f'''
            document.querySelector("{selector}").remove();
            '''

        if cb:
            page.runJavaScript(js, cb)
        else:
            page.runJavaScript(js)

    def _endUpdateVas(self, cb=None):
        js = f'''
        node = document.querySelector('update#updatetmp');
        node.outerHTML = `{self._canv_cache}` + node.outerHTML;
        '''
        if cb:
            self.page().runJavaScript(js, cb)
        else:
            self.page().runJavaScript(js)

        self._canv_cache = None

    def _beginRenderPrepend(self):
        self._canv_cache = ''
        self._canv_ppjump = self._canv_rendvas[0][0]

    def _endRenderPrepend(self, cb=None):
        selector = 'viv:0x%.8x' % self._canv_ppjump
        js = f'''
        var node = document.querySelector("{self._canv_rendtagid}");
        node.innerHTML = `{self._canv_cache}` + node.innerHTML

        var snode = document.getElementsByName("{selector}");
        if (snode != null && snode.length > 0) {{
            snode[0].scrollIntoView()
        }}
        '''
        self._canv_cache = None
        if cb:
            self.page().runJavaScript(js, cb)
        else:
            self.page().runJavaScript(js)

    def _beginRenderAppend(self):
        self._canv_cache = ''

    def _endRenderAppend(self, cb=None):
        page = self.page()
        js = f'document.querySelector("{self._canv_rendtagid}").innerHTML += `{self._canv_cache}`;'
        self._canv_cache = None
        if cb:
            page.runJavaScript(js, cb)
        else:
            page.runJavaScript(js)

    def getNameTag(self, name, typename='name'):
        '''
        Return a "tag" for this memory canvas.  In the case of the
        qt tags, they are a tuple of html text (<opentag>, <closetag>)
        '''
        clsname = 'envi-%s' % typename
        namehex = binascii.hexlify(name.lower()).decode('utf-8')
        subclsname = 'envi-%s-%s' % (typename, namehex)
        return (
            '<span class="%s %s" envitag="%s" envival="%s" onclick="nameclick(this)">'
            % (clsname, subclsname, typename, namehex), '</span>')

    def getVaTag(self, va):
        # The "class" will be the same that we get back from goto event
        return (
            '<span class="envi-va envi-va-0x%.8x" va="0x%.8x" ondblclick="vagoto(this)" oncontextmenu="vaclick(this)" onclick="vaclick(this)">'
            % (va, va), '</span>')

    @QtCore.pyqtSlot(str)
    def _jsGotoExpr(self, expr):
        # The routine used by the javascript code to trigger nav events
        if self._canv_navcallback:
            self._canv_navcallback(expr)

    @QtCore.pyqtSlot(str)
    def _jsSetCurVa(self, vastr):
        self._canv_curva = int(str(vastr), 0)

    # NOTE: doing append / scroll seperately allows render to catch up
    @idlethread
    def _appendInside(self, text, cb=None):
        page = self.page()
        js = f'document.querySelector("{self._canv_rendtagid}").innerHTML += `{text}`;'
        if cb:
            page.runJavaScript(js, cb)
        else:
            page.runJavaScript(js)

    def _add_raw(self, text, cb=None):
        # If we are in a call to renderMemory, cache til the end.
        if self._canv_cache is not None:
            self._canv_cache += text
            return

        self._appendInside(text, cb)

    def addText(self, text, tag=None, cb=None):
        text = html.escape(text)
        #text = text.replace('\n', '<br>')
        if tag is not None:
            otag, ctag = tag
            text = otag + text + ctag
        self._add_raw(text, cb)

    @idlethreadsync
    def clearCanvas(self):
        page = self.page()
        page.runJavaScript(f'''
        var node = document.querySelector("{self._canv_rendtagid}");
        if (node != null) {{
            node.innerHTML = "";
        }}
        ''')

    def contextMenuEvent(self, event):
        va = self._canv_curva
        menu = QMenu()
        if self._canv_curva is not None:
            self.initMemWindowMenu(va, menu)

        viewmenu = menu.addMenu('view   ')
        viewmenu.addAction("Save frame to HTML", ACT(self._menuSaveToHtml))

        menu.exec_(event.globalPos())

    def initMemWindowMenu(self, va, menu):
        initMemSendtoMenu('0x%.8x' % va, menu)

    def dumpHtml(self, data):
        if self.fname:
            with open(self.fname, 'w') as f:
                f.write(data)
            self.fname = None

    def _menuSaveToHtml(self):
        fname = getSaveFileName(self, 'Save As HTML...')
        if fname is not None:
            fname = str(fname)
            if len(fname):
                self.fname = fname
                self.page().toHtml(self.dumpHtml)
Beispiel #38
0
class BrowserView(QMainWindow):
    instances = {}
    inspector_port = None  # The localhost port at which the Remote debugger listens

    create_window_trigger = QtCore.pyqtSignal(object)
    set_title_trigger = QtCore.pyqtSignal(str)
    load_url_trigger = QtCore.pyqtSignal(str)
    html_trigger = QtCore.pyqtSignal(str, str)
    dialog_trigger = QtCore.pyqtSignal(int, str, bool, str, str)
    destroy_trigger = QtCore.pyqtSignal()
    fullscreen_trigger = QtCore.pyqtSignal()
    window_size_trigger = QtCore.pyqtSignal(int, int)
    current_url_trigger = QtCore.pyqtSignal()
    evaluate_js_trigger = QtCore.pyqtSignal(str, str)

    class JSBridge(QtCore.QObject):
        api = None
        parent_uid = None
        qtype = QtCore.QJsonValue if is_webengine else str

        def __init__(self):
            super(BrowserView.JSBridge, self).__init__()

        @QtCore.pyqtSlot(str, qtype, result=str)
        def call(self, func_name, param):
            func_name = BrowserView._convert_string(func_name)
            param = BrowserView._convert_string(param)

            return _js_bridge_call(self.parent_uid, self.api, func_name, param)

    class WebView(QWebView):
        def __init__(self, parent=None):
            super(BrowserView.WebView, self).__init__(parent)

            if parent.frameless:
                QApplication.instance().installEventFilter(self)
                self.setMouseTracking(True)

        def contextMenuEvent(self, event):
            menu = self.page().createStandardContextMenu()

            # If 'Inspect Element' is present in the default context menu, it
            # means the inspector is already up and running.
            for i in menu.actions():
                if i.text() == 'Inspect Element':
                    break
            else:
                # Inspector is not up yet, so create a pseudo 'Inspect Element'
                # menu that will fire it up.
                inspect_element = QAction('Inspect Element', menu)
                inspect_element.triggered.connect(self.show_inspector)
                menu.addAction(inspect_element)

            menu.exec_(event.globalPos())

        # Create a new webview window pointing at the Remote debugger server
        def show_inspector(self):
            uid = self.parent().uid + '-inspector'
            try:
                # If inspector already exists, bring it to the front
                BrowserView.instances[uid].raise_()
                BrowserView.instances[uid].activateWindow()
            except KeyError:
                title = 'Web Inspector - {}'.format(self.parent().title)
                url = 'http://localhost:{}'.format(BrowserView.inspector_port)
                inspector = BrowserView(uid, title, url, 700, 500, True, False, (300, 200),
                                        False, '#fff', False, None, True, self.parent().webview_ready)
                inspector.show()

        def mousePressEvent(self, event):
            if event.button() == QtCore.Qt.LeftButton:
                self.drag_pos = event.globalPos() - self.parent().frameGeometry().topLeft()

            event.accept()

        def mouseMoveEvent(self, event):
            if self.parent().frameless and int(event.buttons()) == 1:  # left button is pressed
                self.parent().move(event.globalPos() - self.drag_pos)

        def eventFilter(self, object, event):
            if object.parent() == self:
                if event.type() == QtCore.QEvent.MouseMove:
                    self.mouseMoveEvent(event)
                elif event.type() == QtCore.QEvent.MouseButtonPress:
                    self.mousePressEvent(event)

            return False

    # New-window-requests handler for Qt 5.5+ only
    class NavigationHandler(QWebPage):
        def __init__(self, parent=None):
            super(BrowserView.NavigationHandler, self).__init__(parent)

        def acceptNavigationRequest(self, url, type, is_main_frame):
            webbrowser.open(url.toString(), 2, True)
            return False

    class WebPage(QWebPage):
        def __init__(self, parent=None):
            super(BrowserView.WebPage, self).__init__(parent)
            self.nav_handler = BrowserView.NavigationHandler(self) if is_webengine else None

        if not is_webengine:
            def acceptNavigationRequest(self, frame, request, type):
                if frame is None:
                    webbrowser.open(request.url().toString(), 2, True)
                    return False
                return True

        def createWindow(self, type):
            return self.nav_handler

    def __init__(self, uid, title, url, width, height, resizable, fullscreen,
                 min_size, confirm_quit, background_color, debug, js_api, text_select, frameless, webview_ready):
        super(BrowserView, self).__init__()
        BrowserView.instances[uid] = self
        self.uid = uid

        self.js_bridge = BrowserView.JSBridge()
        self.js_bridge.api = js_api
        self.js_bridge.parent_uid = self.uid

        self.is_fullscreen = False
        self.confirm_quit = confirm_quit
        self.text_select = text_select

        self._file_name_semaphore = Semaphore(0)
        self._current_url_semaphore = Semaphore(0)

        self.load_event = Event()
        self.webview_ready = webview_ready

        self._js_results = {}
        self._current_url = None
        self._file_name = None

        self.resize(width, height)
        self.title = title
        self.setWindowTitle(title)

        # Set window background color
        self.background_color = QColor()
        self.background_color.setNamedColor(background_color)
        palette = self.palette()
        palette.setColor(self.backgroundRole(), self.background_color)
        self.setPalette(palette)

        if not resizable:
            self.setFixedSize(width, height)

        self.setMinimumSize(min_size[0], min_size[1])

        self.frameless = frameless
        if frameless:
            self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)

        self.view = BrowserView.WebView(self)

        if debug and is_webengine:
            # Initialise Remote debugging (need to be done only once)
            if not BrowserView.inspector_port:
                BrowserView.inspector_port = BrowserView._get_free_port()
                os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = BrowserView.inspector_port
        else:
            self.view.setContextMenuPolicy(QtCore.Qt.NoContextMenu)  # disable right click context menu

        self.view.setPage(BrowserView.WebPage(self.view))

        if url is not None:
            self.view.setUrl(QtCore.QUrl(url))
        else:
            self.load_event.set()

        self.setCentralWidget(self.view)

        self.create_window_trigger.connect(BrowserView.on_create_window)
        self.load_url_trigger.connect(self.on_load_url)
        self.html_trigger.connect(self.on_load_html)
        self.dialog_trigger.connect(self.on_file_dialog)
        self.destroy_trigger.connect(self.on_destroy_window)
        self.fullscreen_trigger.connect(self.on_fullscreen)
        self.window_size_trigger.connect(self.on_window_size)
        self.current_url_trigger.connect(self.on_current_url)
        self.evaluate_js_trigger.connect(self.on_evaluate_js)
        self.set_title_trigger.connect(self.on_set_title)

        if is_webengine and platform.system() != 'OpenBSD':
            self.channel = QWebChannel(self.view.page())
            self.view.page().setWebChannel(self.channel)

        self.view.page().loadFinished.connect(self.on_load_finished)

        if fullscreen:
            self.toggle_fullscreen()

        self.move(QApplication.desktop().availableGeometry().center() - self.rect().center())
        self.activateWindow()
        self.raise_()
        webview_ready.set()

    def on_set_title(self, title):
        self.setWindowTitle(title)

    def on_file_dialog(self, dialog_type, directory, allow_multiple, save_filename, file_filter):
        if dialog_type == FOLDER_DIALOG:
            self._file_name = QFileDialog.getExistingDirectory(self, localization['linux.openFolder'], options=QFileDialog.ShowDirsOnly)
        elif dialog_type == OPEN_DIALOG:
            if allow_multiple:
                self._file_name = QFileDialog.getOpenFileNames(self, localization['linux.openFiles'], directory, file_filter)
            else:
                self._file_name = QFileDialog.getOpenFileName(self, localization['linux.openFile'], directory, file_filter)
        elif dialog_type == SAVE_DIALOG:
            if directory:
                save_filename = os.path.join(str(directory), str(save_filename))

            self._file_name = QFileDialog.getSaveFileName(self, localization['global.saveFile'], save_filename)

        self._file_name_semaphore.release()

    def on_current_url(self):
        url = BrowserView._convert_string(self.view.url().toString())
        self._current_url = None if url == '' else url
        self._current_url_semaphore.release()

    def on_load_url(self, url):
        self.view.setUrl(QtCore.QUrl(url))

    def on_load_html(self, content, base_uri):
        self.view.setHtml(content, QtCore.QUrl(base_uri))

    def closeEvent(self, event):
        if self.confirm_quit:
            reply = QMessageBox.question(self, self.title, localization['global.quitConfirmation'],
                                         QMessageBox.Yes, QMessageBox.No)

            if reply == QMessageBox.No:
                event.ignore()
                return

        event.accept()
        del BrowserView.instances[self.uid]

        try:    # Close inspector if open
            BrowserView.instances[self.uid + '-inspector'].close()
            del BrowserView.instances[self.uid + '-inspector']
        except KeyError:
            pass

        if len(BrowserView.instances) == 0:
            _app.exit()

    def on_destroy_window(self):
        self.close()

    def on_fullscreen(self):
        if self.is_fullscreen:
            self.showNormal()
        else:
            self.showFullScreen()

        self.is_fullscreen = not self.is_fullscreen

    def on_window_size(self, width, height):
        self.setFixedSize(width, height)

    def on_evaluate_js(self, script, uuid):
        def return_result(result):
            result = BrowserView._convert_string(result)
            uuid_ = BrowserView._convert_string(uuid)

            js_result = self._js_results[uuid_]
            js_result['result'] = None if result is None or result == 'null' else result if result == '' else json.loads(result)
            js_result['semaphore'].release()

        try:    # < Qt5.6
            result = self.view.page().mainFrame().evaluateJavaScript(script)
            return_result(result)
        except AttributeError:
            self.view.page().runJavaScript(script, return_result)


    def on_load_finished(self):
        if self.js_bridge.api:
            self._set_js_api()
        else:
            self.load_event.set()

        if not self.text_select:
            script = disable_text_select.replace('\n', '')

            try:  # QT < 5.6
                self.view.page().mainFrame().evaluateJavaScript(script)
            except AttributeError:
                self.view.page().runJavaScript(script)

    def set_title(self, title):
        self.set_title_trigger.emit(title)

    def get_current_url(self):
        self.load_event.wait()
        self.current_url_trigger.emit()
        self._current_url_semaphore.acquire()

        return self._current_url

    def load_url(self, url):
        self.load_event.clear()
        self.load_url_trigger.emit(url)

    def load_html(self, content, base_uri):
        self.load_event.clear()
        self.html_trigger.emit(content, base_uri)

    def create_file_dialog(self, dialog_type, directory, allow_multiple, save_filename, file_filter):
        self.dialog_trigger.emit(dialog_type, directory, allow_multiple, save_filename, file_filter)
        self._file_name_semaphore.acquire()

        if dialog_type == FOLDER_DIALOG:
            file_names = (self._file_name,)
        elif dialog_type == SAVE_DIALOG or not allow_multiple:
            file_names = (self._file_name[0],)
        else:
            file_names = tuple(self._file_name[0])

        # Check if we got an empty tuple, or a tuple with empty string
        if len(file_names) == 0 or len(file_names[0]) == 0:
            return None
        else:
            return file_names

    def destroy_(self):
        self.destroy_trigger.emit()

    def toggle_fullscreen(self):
        self.fullscreen_trigger.emit()

    def set_window_size(self, width, height):
        self.window_size_trigger.emit(width, height)

    def evaluate_js(self, script):
        self.load_event.wait()
        result_semaphore = Semaphore(0)
        unique_id = uuid1().hex
        self._js_results[unique_id] = {'semaphore': result_semaphore, 'result': ''}

        self.evaluate_js_trigger.emit(script, unique_id)
        result_semaphore.acquire()

        result = deepcopy(self._js_results[unique_id]['result'])
        del self._js_results[unique_id]

        return result

    def _set_js_api(self):
        def _register_window_object():
            frame.addToJavaScriptWindowObject('external', self.js_bridge)

        script = parse_api_js(self.js_bridge.api)

        if is_webengine:
            qwebchannel_js = QtCore.QFile('://qtwebchannel/qwebchannel.js')
            if qwebchannel_js.open(QtCore.QFile.ReadOnly):
                source = bytes(qwebchannel_js.readAll()).decode('utf-8')
                self.view.page().runJavaScript(source)
                self.channel.registerObject('external', self.js_bridge)
                qwebchannel_js.close()
        else:
            frame = self.view.page().mainFrame()
            _register_window_object()

        try:    # < QT 5.6
            self.view.page().mainFrame().evaluateJavaScript(script)
        except AttributeError:
            self.view.page().runJavaScript(script)
        self.load_event.set()

    @staticmethod
    def _convert_string(result):
        try:
            if result is None or result.isNull():
                return None

            result = result.toString() # QJsonValue conversion
        except AttributeError:
            pass

        return convert_string(result)

    @staticmethod
    # A simple function to obtain an unused localhost port from the os return it
    def _get_free_port():
        s = socket()
        s.bind(('localhost', 0))
        port = str(s.getsockname()[1])
        s.close()
        return port

    @staticmethod
    # Receive func from subthread and execute it on the main thread
    def on_create_window(func):
        func()
class BrowserWidget(QWebEngineView, VIGITIABaseApplication):
    """ BrowserWidget

        Example application demonstrating how to display Webpages and local HTML Files on the table

    """

    # Flag to toggle visibility
    is_hidden = False

    def __init__(self, rendering_manager):
        super().__init__()
        self.set_name(self.__class__.__name__)
        self.set_rendering_manager(rendering_manager)

        self.x = 2500
        self.y = 1000
        self.width = 1920
        self.height = 1080
        self.rotation = 0
        self.z_index = 100

        # Prevent ESC from beeing blocked by BrowserWidget
        # https://stackoverflow.com/questions/56890831/qwidget-cannot-catch-escape-backspace-or-c-x-key-press-events
        QShortcut(QKeySequence("Escape"), self, activated=self.on_Escape)

        self.initUI()

        # Uncomment this line to establish connection to js code
        # self.connect_to_javascript_code()

        # Define url or local index.html path here
        url = QUrl('https://maps.google.com')
        # Explanation on how to load a local HTML page
        # url = QUrl.fromLocalFile(os.path.abspath(os.path.join(os.path.dirname(__file__), "index.html")))

        self.load(url)

    def connect_to_javascript_code(self):
        self.channel = QWebChannel()
        self.handler = CallHandler()
        self.channel.registerObject('handler', self.handler)
        self.page().setWebChannel(self.channel)
        self.loadFinished.connect(self.loadFinishedHandler)

    def initUI(self):
        pass
        #self.setGeometry(0, 0, self.width, self.height)

    @pyqtSlot()
    def loadFinishedHandler(self):
        # Example on how to call a function in the javascript code of a website
        js_code = 'pythonToJS("I am a message from python");'
        self.page().runJavaScript(js_code, self.js_callback)

    def js_callback(self, result):
        print('Python called back:', result)

    def on_new_token_messages(self, data):
        # print('Tokens:', data)
        for token in data:
            if token['component_id'] == 4:
                angle = token['angle']
                self.set_rotation(angle)
                #self.set_position(token['x_pos'], token['y_pos'])
                #self.set_x(token['x_pos'])
                #self.set_y(token['y_pos'])

    def on_key_pressed(self, event):
        if event.key() == Qt.Key_F1:
            #self.set_rotation(self.rotation + 10)
            self.set_rotation(random.randint(0, 359))
        if event.key() == Qt.Key_F2:
            #self.set_position(self.get_x() - 10, self.get_y())
            self.set_position(1845, 750)

    # Use Touch events in the application: Convert the pointer messages to mouse click events
    def on_new_pointer_messages(self, messages):
        for message in messages:

            global_pos = QPoint(message['x_pos'], message['y_pos'])
            local_pos = self.mapFromGlobal(global_pos)

            #target = self.focusProxy()
            target = self

            self.emulate_mouse_event(QEvent.MouseMove, local_pos, global_pos,
                                     target)
            self.emulate_mouse_event(QEvent.MouseButtonPress, local_pos,
                                     global_pos, target)

    def on_new_control_messages(self, data):
        pass
        # Show/hide video
        # if data[2] == 3 and data[3] == 1:
        #     self.is_hidden = not self.is_hidden
        #
        # if self.is_hidden:
        #     self.hide()
        # else:
        #     self.show()

    #
    def emulate_mouse_event(self, event_type, local_pos, global_pos, target):
        mouse_event = QMouseEvent(event_type, local_pos, global_pos,
                                  Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
        try:
            QCoreApplication.sendEvent(target, mouse_event)
        except:
            print(
                '[BrowserWidget]: Error on injecting Touch Event in BrowserWidget'
            )

    @pyqtSlot()
    def on_Escape(self):
        self.rendering_manager.close_window()