class TermView(WebView): """XTerm Wrapper.""" def __init__(self, parent, CONF, term_url='http://127.0.0.1:8070', handler=None): """Webview main constructor.""" WebView.__init__(self, parent) self.parent = parent self.CONF = CONF self.copy_action = create_action(self, _("Copy text"), icon=ima.icon('editcopy'), triggered=self.copy, shortcut=self.CONF.get_shortcut( CONF_SECTION, 'copy')) self.paste_action = create_action(self, _("Paste text"), icon=ima.icon('editpaste'), triggered=self.paste, shortcut=self.CONF.get_shortcut( CONF_SECTION, 'paste')) self.clear_action = create_action(self, _("Clear Terminal"), triggered=self.clear, shortcut=self.CONF.get_shortcut( CONF_SECTION, 'clear')) if WEBENGINE: self.channel = QWebChannel(self.page()) self.page().setWebChannel(self.channel) self.channel.registerObject('handler', handler) self.term_url = QUrl(term_url) self.load(self.term_url) if WEBENGINE: self.document = self.page() try: self.document.profile().clearHttpCache() except AttributeError: pass else: self.document = self.page().mainFrame() self.initial_y_pos = 0 self.setFocusPolicy(Qt.ClickFocus) def copy(self): """Copy unicode text from terminal.""" self.triggerPageAction(QWebEnginePage.Copy) def paste(self): """Paste unicode text into terminal.""" self.triggerPageAction(QWebEnginePage.Paste) def clear(self): """Clear the terminal.""" self.eval_javascript('clearTerm()') def contextMenuEvent(self, event): """Override Qt method.""" menu = QMenu(self) actions = [ self.pageAction(QWebEnginePage.SelectAll), self.copy_action, self.paste_action ] if DEV and not WEBENGINE: settings = self.page().settings() settings.setAttribute(QWebEngineSettings.DeveloperExtrasEnabled, True) actions += [None, self.pageAction(QWebEnginePage.InspectElement)] add_actions(menu, actions) menu.popup(event.globalPos()) event.accept() def eval_javascript(self, script): """ Evaluate Javascript instructions inside DOM with the expected prefix. """ script = PREFIX + script if WEBENGINE: self.document.runJavaScript("{}".format(script), self.return_js_value) else: self.document.evaluateJavaScript("{}".format(script)) def return_js_value(self, value): """Return the value of the function evaluated in Javascript.""" return value def wheelEvent(self, event): """Catch and process wheel scrolling events via Javascript.""" delta = event.angleDelta().y() self.eval_javascript('scrollTerm({0})'.format(delta)) def event(self, event): """Grab all keyboard input.""" if event.type() == QEvent.ShortcutOverride: key = event.key() modifiers = event.modifiers() if modifiers & Qt.ShiftModifier: key += Qt.SHIFT if modifiers & Qt.ControlModifier: key += Qt.CTRL if modifiers & Qt.AltModifier: key += Qt.ALT if modifiers & Qt.MetaModifier: key += Qt.META sequence = QKeySequence(key).toString(QKeySequence.PortableText) if sequence == self.CONF.get_shortcut(CONF_SECTION, 'copy'): self.copy() elif sequence == self.CONF.get_shortcut(CONF_SECTION, 'paste'): self.paste() elif sequence == self.CONF.get_shortcut(CONF_SECTION, 'clear'): self.clear() else: event.ignore() return False event.accept() return True return WebView.event(self, event)
class TermView(QWebEngineView): """XTerm Wrapper.""" def __init__(self, parent, CONF, term_url='http://127.0.0.1:8070', handler=None): """Webview main constructor.""" super().__init__(parent) web_page = QWebEnginePage(self) self.setPage(web_page) self.source_text = '' self.parent = parent self.CONF = CONF self.shortcuts = self.create_shortcuts() self.channel = QWebChannel(self.page()) self.page().setWebChannel(self.channel) self.channel.registerObject('handler', handler) self.term_url = QUrl(term_url) self.load(self.term_url) self.document = self.page() try: self.document.profile().clearHttpCache() except AttributeError: pass self.initial_y_pos = 0 self.setFocusPolicy(Qt.ClickFocus) def copy(self): """Copy unicode text from terminal.""" self.triggerPageAction(QWebEnginePage.Copy) def paste(self): """Paste unicode text into terminal.""" clipboard = QApplication.clipboard() text = str(clipboard.text()) if len(text.splitlines()) > 1: eol_chars = os.linesep text = eol_chars.join((text + eol_chars).splitlines()) self.eval_javascript('pasteText({})'.format(repr(text))) def clear(self): """Clear the terminal.""" self.eval_javascript('clearTerm()') def increase_font(self): """Increase terminal font.""" return self.eval_javascript('increaseFontSize()') def decrease_font(self): """Decrease terminal font.""" return self.eval_javascript('decreaseFontSize()') def create_shortcuts(self): """Create the terminal shortcuts.""" copy_shortcut = self.CONF.config_shortcut( lambda: self.copy(), context='terminal', name='copy', parent=self) paste_shortcut = self.CONF.config_shortcut( lambda: self.paste(), context='terminal', name='paste', parent=self) clear_shortcut = self.CONF.config_shortcut( lambda: self.clear(), context='terminal', name='clear', parent=self) zoomin_shortcut = self.CONF.config_shortcut( lambda: self.increase_font(), context='terminal', name='zoom in', parent=self) zoomout_shortcut = self.CONF.config_shortcut( lambda: self.decrease_font(), context='terminal', name='zoom out', parent=self) return [copy_shortcut, paste_shortcut, clear_shortcut, zoomin_shortcut, zoomout_shortcut] def get_shortcut_data(self): """ Return shortcut data, a list of tuples (shortcut, text, default). shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def contextMenuEvent(self, event): """Override Qt method.""" copy_action = create_action( self, _("Copy text"), icon=ima.icon('editcopy'), triggered=self.copy, shortcut=self.CONF.get_shortcut(CONF_SECTION, 'copy')) paste_action = create_action( self, _("Paste text"), icon=ima.icon('editpaste'), triggered=self.paste, shortcut=self.CONF.get_shortcut(CONF_SECTION, 'paste')) clear_action = create_action( self, _("Clear Terminal"), triggered=self.clear, shortcut=self.CONF.get_shortcut(CONF_SECTION, 'clear')) zoom_in = create_action( self, _("Zoom in"), triggered=self.increase_font, shortcut=self.CONF.get_shortcut(CONF_SECTION, 'zoom in')) zoom_out = create_action( self, _("Zoom out"), triggered=self.decrease_font, shortcut=self.CONF.get_shortcut(CONF_SECTION, 'zoom out')) menu = QMenu(self) actions = [self.pageAction(QWebEnginePage.SelectAll), copy_action, paste_action, clear_action, None, zoom_in, zoom_out] add_actions(menu, actions) menu.popup(event.globalPos()) event.accept() def eval_javascript(self, script): """ Evaluate Javascript instructions inside DOM with the expected prefix. """ script = PREFIX + script self.document.runJavaScript("{}".format(script), self.return_js_value) def return_js_value(self, value): """Return the value of the function evaluated in Javascript.""" return value def wheelEvent(self, event): """Catch and process wheel scrolling events via Javascript.""" delta = event.angleDelta().y() self.eval_javascript('scrollTerm({0})'.format(delta)) def event(self, event): """Grab all keyboard input.""" if event.type() == QEvent.ShortcutOverride: self.keyPressEvent(event) return True return True def keyPressEvent(self, event): """Qt override method.""" key = event.key() modifiers = event.modifiers() if modifiers & Qt.ShiftModifier: key += Qt.SHIFT if modifiers & Qt.ControlModifier: key += Qt.CTRL if modifiers & Qt.AltModifier: key += Qt.ALT if modifiers & Qt.MetaModifier: key += Qt.META sequence = QKeySequence(key).toString(QKeySequence.PortableText) if event == QKeySequence.Paste: self.paste() elif sequence == self.CONF.get_shortcut(CONF_SECTION, 'copy'): self.copy() elif sequence == self.CONF.get_shortcut(CONF_SECTION, 'paste'): self.paste() elif sequence == self.CONF.get_shortcut(CONF_SECTION, 'clear'): self.clear() elif sequence == self.CONF.get_shortcut( CONF_SECTION, 'zoom in'): self.increase_font() elif sequence == self.CONF.get_shortcut( CONF_SECTION, 'zoom out'): self.decrease_font() else: super().keyPressEvent(event)
class BrowserView(QMainWindow): instances = {} inspector_port = None # The localhost port at which the Remote debugger listens create_window_trigger = QtCore.Signal(object) set_title_trigger = QtCore.Signal(str) load_url_trigger = QtCore.Signal(str) html_trigger = QtCore.Signal(str, str) dialog_trigger = QtCore.Signal(int, str, bool, str, str) destroy_trigger = QtCore.Signal() hide_trigger = QtCore.Signal() show_trigger = QtCore.Signal() fullscreen_trigger = QtCore.Signal() window_size_trigger = QtCore.Signal(int, int, FixPoint) window_move_trigger = QtCore.Signal(int, int) window_minimize_trigger = QtCore.Signal() window_restore_trigger = QtCore.Signal() current_url_trigger = QtCore.Signal() evaluate_js_trigger = QtCore.Signal(str, str) on_top_trigger = QtCore.Signal(bool) class JSBridge(QtCore.QObject): qtype = QtCore.QJsonValue if is_webengine else str def __init__(self): super(BrowserView.JSBridge, self).__init__() @QtCore.Slot(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, json.loads(param), value_id) class WebView(QWebView): def __init__(self, parent=None): super(BrowserView.WebView, self).__init__(parent) if parent.frameless and parent.easy_drag: QApplication.instance().installEventFilter(self) self.setMouseTracking(True) self.transparent = parent.transparent if parent.transparent: self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False) self.setStyleSheet("background: transparent;") 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) print(url) window = Window('web_inspector', title, url, '', 700, 500, None, None, True, False, (300, 200), False, False, False, False, False, False, '#fff', None, False, False, None) window.localization = self.parent().localization 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): parent = self.parent() if parent.frameless and parent.easy_drag and int( event.buttons()) == 1: # left button is pressed 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 parent.transparent: self.setBackgroundColor(QtCore.Qt.transparent) 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 userAgentForUrl(self, url): user_agent = settings.get('user_agent') or _user_agent if user_agent: return user_agent else: return super().userAgentForUrl(url) 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.events.loaded self.shown = window.events.shown self.localization = window.localization self._js_results = {} self._current_url = None self._file_name = None self.resize(window.initial_width, window.initial_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.initial_width, window.initial_height) self.setMinimumSize(window.min_size[0], window.min_size[1]) self.frameless = window.frameless self.easy_drag = window.easy_drag flags = self.windowFlags() if self.frameless: flags = flags | QtCore.Qt.FramelessWindowHint if window.on_top: flags = flags | QtCore.Qt.WindowStaysOnTopHint self.setWindowFlags(flags) self.transparent = window.transparent if self.transparent: # Override the background color self.background_color = QColor('transparent') palette = self.palette() palette.setColor(self.backgroundRole(), self.background_color) self.setPalette(palette) # Enable the transparency hint self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.view = BrowserView.WebView(self) if is_webengine: os.environ['QTWEBENGINE_CHROMIUM_FLAGS'] = ( '--use-fake-ui-for-media-stream --enable-features=AutoplayIgnoreWebAudio' ) if _debug['mode'] and is_webengine: # Initialise Remote debugging (need to be done only once) if not BrowserView.inspector_port: BrowserView.inspector_port = BrowserView._get_debug_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) self.on_top_trigger.connect(self.on_set_on_top) 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.real_url is not None: self.view.setUrl(QtCore.QUrl(window.real_url)) elif window.uid == 'web_inspector': self.view.setUrl(QtCore.QUrl(window.original_url)) elif window.html: self.view.setHtml(window.html, QtCore.QUrl('')) else: self.view.setHtml(default_html, QtCore.QUrl('')) if window.initial_x is not None and window.initial_y is not None: self.move(window.initial_x, window.initial_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, self.localization['linux.openFolder'], options=QFileDialog.ShowDirsOnly) elif dialog_type == OPEN_DIALOG: if allow_multiple: self._file_name = QFileDialog.getOpenFileNames( self, self.localization['linux.openFiles'], directory, file_filter) else: self._file_name = QFileDialog.getOpenFileName( self, 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, 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 on_set_on_top(self, top): flags = self.windowFlags() if top: self.setWindowFlags(flags | QtCore.Qt.WindowStaysOnTopHint) else: self.setWindowFlags(flags & ~QtCore.Qt.WindowStaysOnTopHint) self.show() def closeEvent(self, event): if self.confirm_close: reply = QMessageBox.question( self, self.title, self.localization['global.quitConfirmation'], QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.No: event.ignore() return should_cancel = self.pywebview_window.events.closing.set() if should_cancel: event.ignore() return event.accept() BrowserView.instances[self.uid].close() del BrowserView.instances[self.uid] if self.pywebview_window in windows: windows.remove(self.pywebview_window) self.pywebview_window.events.closed.set() if len(BrowserView.instances) == 0: self.hide() _app.exit() def changeEvent(self, e): if e.type() != QtCore.QEvent.WindowStateChange: return if self.windowState() == QtCore.Qt.WindowMinimized: self.pywebview_window.events.minimized.set() if self.windowState() == QtCore.Qt.WindowMaximized: self.pywebview_window.events.maximized.set() if self.windowState() == QtCore.Qt.WindowNoState and e.oldState() in ( QtCore.Qt.WindowMinimized, QtCore.Qt.WindowMaximized): self.pywebview_window.events.restored.set() def resizeEvent(self, e): if self.pywebview_window.initial_width != self.width() or \ self.pywebview_window.initial_height != self.height(): self.pywebview_window.events.resized.set(self.width(), self.height()) 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, fix_point): geo = self.geometry() if fix_point & FixPoint.EAST: # Keep the right of the window in the same place geo.setX(geo.x() + geo.width() - width) if fix_point & FixPoint.SOUTH: # Keep the top of the window in the same place geo.setY(geo.y() + geo.height() - height) self.setGeometry(geo) 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 self.view.page().runJavaScript(script, return_result) except TypeError: self.view.page().runJavaScript(script) # PySide2 & PySide6 except AttributeError: result = self.view.page().mainFrame().evaluateJavaScript(script) return_result(result) except Exception as e: logger.exception(e) def on_load_finished(self): if self.uid == 'web_inspector': return self._set_js_api() if not self.text_select: script = disable_text_select.replace('\n', '') try: self.view.page().runJavaScript(script) except: # QT < 5.6 self.view.page().mainFrame().evaluateJavaScript(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, fix_point): self.window_size_trigger.emit(width, height, fix_point) 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 set_on_top(self, top): self.on_top_trigger.emit(top) 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, 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: self.view.page().runJavaScript(script) except AttributeError: # < QT 5.6 self.view.page().mainFrame().evaluateJavaScript(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 def _get_debug_port(): """ Check if default debug port 8228 is available, increment it by 1 until a port is available. :return: port: str """ port_available = False port = 8228 while not port_available: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', port)) port_available = True except: port_available = False logger.warning('Port %s is in use' % port) port += 1 finally: sock.close() return str(port) @staticmethod # Receive func from subthread and execute it on the main thread def on_create_window(func): func()
class MapsView(QWebEngineView): """Show Google Maps in Qt app.""" def __init__(self, parent): super(MapsView, self).__init__(parent) self._logger = logging.getLogger(self.__class__.__name__) # Set browser attributes QNetworkProxyFactory.setUseSystemConfiguration(False) self.settings().setAttribute( QWebEngineSettings.Accelerated2dCanvasEnabled, True) self.settings().setAttribute(QWebEngineSettings.JavascriptEnabled, True) # Create maps page (it is not needed, but we can handle Javascript console logs) self._mapspage = MapsPage(self) self.setPage(self._mapspage) # Create connection between Javascript and Qt self.channel = QWebChannel(self.page()) self.page().setWebChannel(self.channel) # create map events handler and register it as "jshelper" in HTML self.handler = CallHandler(self) self.handler.runJavascript.connect(self.runScript) self.loadFinished.connect(self.handler.on_loadFinished) self.loadStarted.connect(self.handler.on_loadStarted) self.channel.registerObject("jshelper", self.handler) @Slot(str, object) def runScript(self, script, callback): """Run Javascript code. Args: script (str): Script to execute. callback (callback, optional): Function to handle callback. """ if callback is None: self.page().runJavaScript(script) else: self.page().runJavaScript(script, callback) def getHandler(self): """Returns map event handler.""" return self.handler def enableMarkersDragging(self, value): """Enable or disable markers dragging feature. Args: value (bool): Enable markers dragging. """ self.handler.enableMarkersDragging(value) def addMarker(self, marker_id, lat, lng, options=None): """Creates marker with marker_id id at latitude, longitude. Args: marker_id (int): Marker ID. lat (float): Marker latitude. lng (float): Marker longitude. options (dict): Marker options. """ if options is None: options = {} self.handler.addMarker(marker_id, lat, lng, options) def deleteMarker(self, marker_id): """Delete marker with ID. Args: marker_id (int): Marker ID. """ self.handler.deleteMarker(marker_id) def updateMarker(self, marker_id, options): """Delete marker with ID. Args: marker_id (int): Marker ID. options (dict): Marker options. """ self.handler.updateMarker(marker_id, options) def moveMarker(self, marker_id, lat, lng): """Move marker to location with (lat, lng). Args: marker_id (int): Marker ID. lat (float): Marker latitude. lng (float): Marker longitude. """ self.handler.moveMarker(marker_id, lat, lng) def deletePolyline(self, polyline_id): """Delete polyline with ID. Args: polyline_id (int): Polyline ID. """ self.handler.deletePolyline(polyline_id) def addPolyline(self, polyline_id, coords: list): """Creates polyline using coordinates. Args: polyline_id (int): Polyline id coords (list): List of coordinates (dicts with "lat" and "lng" keys). """ self.handler.addPolyline(polyline_id, coords) def addPolylineBetweenMarkers(self, polyline_id, markers: list): """Creates polyline using coordinates. Args: polyline_id (int): Polyline id markers (list): List of markers IDs. """ self.handler.addPolylineBetweenMarkers(polyline_id, markers) def panToCenter(self): """Pan map to center.""" self.handler.panToCenter() def disableMapDragging(self, value): """Enable or disable map dragging. Args: value (bool): Map dragging status. """ self.handler.disableMapDragging(value) def showZoomControl(self, value): """Show or hide zoom control widget. Args: value (bool): Zoom control widget status. """ self.handler.showZoomControl(value) def disableDoubleClickToZoom(self, value): """Enable or disable double click to zoom. Args: value (bool): Double click to zoom status. """ self.handler.disableDoubleClickToZoom(value) def disableScrollWheel(self, value): """Enable or disable scroll to zoom. Args: value (bool): Scroll to zoom status. """ self.handler.disableScrollWheel(value)
class TermView(QWebEngineView, SpyderWidgetMixin): """XTerm Wrapper.""" sig_focus_in_event = Signal() """ This signal is emitted when the widget receives focus. """ sig_focus_out_event = Signal() """ This signal is emitted when the widget loses focus. """ def __init__(self, parent, term_url='http://127.0.0.1:8070', handler=None): """Webview main constructor.""" super().__init__(parent, class_parent=parent) web_page = QWebEnginePage(self) self.setPage(web_page) self.source_text = '' self.parent = parent self.channel = QWebChannel(self.page()) self.page().setWebChannel(self.channel) self.channel.registerObject('handler', handler) self.term_url = QUrl(term_url) self.load(self.term_url) self.focusProxy().installEventFilter(self) self.document = self.page() try: self.document.profile().clearHttpCache() except AttributeError: pass self.initial_y_pos = 0 self.setFocusPolicy(Qt.ClickFocus) self.setup() def setup(self): """Create the terminal context menu.""" # Create context menu self.context_menu = self.get_menu(TermViewMenus.Context) for item in [self.get_action(TerminalMainWidgetActions.Copy), self.get_action(TerminalMainWidgetActions.Paste), self.get_action(TerminalMainWidgetActions.Clear)]: self.add_item_to_menu( item, menu=self.context_menu, section=TermViewSections.CommonActions, ) for item in [self.get_action(TerminalMainWidgetActions.ZoomIn), self.get_action(TerminalMainWidgetActions.ZoomOut)]: self.add_item_to_menu( item, menu=self.context_menu, section=TermViewSections.ZoomActions, ) def copy(self): """Copy unicode text from terminal.""" self.triggerPageAction(QWebEnginePage.Copy) def paste(self): """Paste unicode text into terminal.""" clipboard = QApplication.clipboard() text = str(clipboard.text()) if len(text.splitlines()) > 1: eol_chars = os.linesep text = eol_chars.join((text + eol_chars).splitlines()) self.eval_javascript('pasteText({})'.format(repr(text))) def clear(self): """Clear the terminal.""" self.eval_javascript('clearTerm()') def increase_font(self): """Increase terminal font.""" zoom = self.get_conf('zoom') self.set_conf('zoom', zoom + 1) return self.eval_javascript('increaseFontSize()') def decrease_font(self): """Decrease terminal font.""" zoom = self.get_conf('zoom') self.set_conf('zoom', zoom - 1) return self.eval_javascript('decreaseFontSize()') def contextMenuEvent(self, event): """Override Qt method.""" self.context_menu.popup(event.globalPos()) event.accept() def eval_javascript(self, script): """ Evaluate Javascript instructions inside DOM with the expected prefix. """ script = PREFIX + script self.document.runJavaScript("{}".format(script), self.return_js_value) def return_js_value(self, value): """Return the value of the function evaluated in Javascript.""" return value def wheelEvent(self, event): """Catch and process wheel scrolling events via Javascript.""" delta = event.angleDelta().y() self.eval_javascript('scrollTerm({0})'.format(delta)) def event(self, event): """Grab all keyboard input.""" if event.type() == QEvent.ShortcutOverride: self.keyPressEvent(event) return True return True def keyPressEvent(self, event): """Qt override method.""" key = event.key() modifiers = event.modifiers() if modifiers & Qt.ShiftModifier: key += Qt.SHIFT if modifiers & Qt.ControlModifier: key += Qt.CTRL if modifiers & Qt.AltModifier: key += Qt.ALT if modifiers & Qt.MetaModifier: key += Qt.META sequence = QKeySequence(key).toString(QKeySequence.PortableText) if event == QKeySequence.Paste: self.paste() elif sequence == self.get_shortcut('copy'): self.copy() elif sequence == self.get_shortcut('paste'): self.paste() elif sequence == self.get_shortcut('clear'): self.clear() elif sequence == self.get_shortcut('zoom_in'): self.increase_font() elif sequence == self.get_shortcut('zoom_out'): self.decrease_font() else: super().keyPressEvent(event) def eventFilter(self, widget, event): """ Handle events that affect the view. All events (e.g. focus in/out) reach the focus proxy, not this widget itself. That's why this event filter is necessary. """ if self.focusProxy() is widget: if event.type() == QEvent.FocusIn: self.sig_focus_in_event.emit() elif event.type() == QEvent.FocusOut: self.sig_focus_out_event.emit() return super().eventFilter(widget, event)
class WebEngineView(QWebEngineView): customSignal = Signal(str, str) saveSignal = Signal() signal_open_file = Signal(str, str) signal_request_text = Signal() signal_text_got = Signal(str) signal_save_as = Signal(str) signal_set_autocomplete_apis = Signal(str) def __init__(self, *args, **kwargs): super(WebEngineView, self).__init__(*args, **kwargs) self._untitled_id = 0 self.initSettings() self.channel = QWebChannel(self) # 把自身对象传递进去 self.channel.registerObject('Bridge', self) # 设置交互接口 self.page().setWebChannel(self.channel) # self.signal_set_autocomplete_apis.emit({"keywords": ["aaaaa", "bbbbbb"]}) @Slot(str, str) def on_text_received(self, path, text): print(text) self.signal_text_got.emit(text) # 注意pyqtSlot用于把该函数暴露给js可以调用 @Slot(str) def print_from_js(self, text): print('print from js', text) @Slot(str, str) def callFromJs(self, file, text): print('call from js!') try: with open(file, mode='w', encoding='utf-8') as f: f.write(text.replace('\r', '')) f.close() except Exception as e: print(e) @Slot(str, str) def on_save(self, file_name: str, text: str): if os.path.isabs(file_name): pass else: file_name, ext = QFileDialog.getSaveFileName(self, "aaa", "/home/hzy/Desktop", "All Files(*)") print(file_name, text) with open(file_name, 'w') as f: f.write(text) self.signal_save_as.emit(file_name) def open_file(self, file: str): """ 打开文件 Args: file: Returns: """ with open(file, 'r', encoding='utf-8') as f: text = f.read() self.signal_open_file.emit(file, text) def new_file(self): self._untitled_id += 1 self.signal_open_file.emit("Untitled-%d.py" % self._untitled_id, "") def sendCustomSignal(self, file): # 发送自定义信号 with open(file, 'r', encoding='utf-8') as f: text = f.read() self.customSignal.emit(file, text) def sendSaveSignal(self): self.saveSignal.emit() # @Slot(str) # @Slot(QUrl) def load(self, url): ''' eg: load("https://qtpy.com") :param url: 网址 ''' return super(WebEngineView, self).load(QUrl(url)) def initSettings(self): ''' eg: 初始化设置 ''' # 获取浏览器默认设置 settings = QWebEngineSettings.globalSettings() # 设置默认编码utf8 settings.setDefaultTextEncoding("utf-8") # 自动加载图片,默认开启 # settings.setAttribute(QWebEngineSettings.AutoLoadImages,True) # 自动加载图标,默认开启 # settings.setAttribute(QWebEngineSettings.AutoLoadIconsForPage,True) # 开启js,默认开启 # settings.setAttribute(QWebEngineSettings.JavascriptEnabled,True) # js可以访问剪贴板 settings.setAttribute( QWebEngineSettings.JavascriptCanAccessClipboard, True) # js可以打开窗口,默认开启 # settings.setAttribute(QWebEngineSettings.JavascriptCanOpenWindows,True) # 链接获取焦点时的状态,默认开启 # settings.setAttribute(QWebEngineSettings.LinksIncludedInFocusChain,True) # 本地储存,默认开启 # settings.setAttribute(QWebEngineSettings.LocalStorageEnabled,True) # 本地访问远程 settings.setAttribute( QWebEngineSettings.LocalContentCanAccessRemoteUrls, True) # 本地加载,默认开启 # settings.setAttribute(QWebEngineSettings.LocalContentCanAccessFileUrls,True) # 监控负载要求跨站点脚本,默认关闭 # settings.setAttribute(QWebEngineSettings.XSSAuditingEnabled,False) # 空间导航特性,默认关闭 # settings.setAttribute(QWebEngineSettings.SpatialNavigationEnabled,False) # 支持平超链接属性,默认关闭 # settings.setAttribute(QWebEngineSettings.HyperlinkAuditingEnabled,False) # 使用滚动动画,默认关闭 settings.setAttribute(QWebEngineSettings.ScrollAnimatorEnabled, True) # 支持错误页面,默认启用 # settings.setAttribute(QWebEngineSettings.ErrorPageEnabled, True) # 支持插件,默认关闭 settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) # 支持全屏应用程序,默认关闭 settings.setAttribute( QWebEngineSettings.FullScreenSupportEnabled, True) # 支持屏幕截屏,默认关闭 settings.setAttribute(QWebEngineSettings.ScreenCaptureEnabled, True) # 支持html5 WebGl,默认开启 settings.setAttribute(QWebEngineSettings.WebGLEnabled, True) # 支持2d绘制,默认开启 settings.setAttribute( QWebEngineSettings.Accelerated2dCanvasEnabled, True) # 支持图标触摸,默认关闭 settings.setAttribute(QWebEngineSettings.TouchIconsEnabled, True)