qutebrowser/qutebrowser/browser/webkit/webkittab.py

833 lines
30 KiB
Python
Raw Normal View History

2016-06-13 14:44:41 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2018-02-05 12:19:50 +01:00
# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2016-06-13 14:44:41 +02:00
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Wrapper over our (QtWebKit) WebView."""
import re
2016-07-04 15:18:49 +02:00
import functools
2016-07-04 13:35:38 +02:00
import xml.etree.ElementTree
2016-06-14 18:08:46 +02:00
import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize)
from PyQt5.QtGui import QKeyEvent, QIcon
2016-07-27 15:34:28 +02:00
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
2016-06-14 18:08:46 +02:00
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
2016-06-13 14:44:41 +02:00
from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
webkitsettings)
2018-06-11 15:30:01 +02:00
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
2016-06-14 18:08:46 +02:00
2017-01-02 20:02:02 +01:00
class WebKitAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions."""
2017-05-09 22:02:30 +02:00
action_class = QWebPage
action_base = QWebPage.WebAction
2017-01-02 20:02:02 +01:00
def exit_fullscreen(self):
raise browsertab.UnsupportedOperationError
2017-02-07 00:15:39 +01:00
def save_page(self):
"""Save the current page."""
raise browsertab.UnsupportedOperationError
2018-03-01 16:13:00 +01:00
def show_source(self, pygments=False):
self._show_source_pygments()
2017-01-02 20:02:02 +01:00
class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing."""
def check_pdf_support(self):
2017-09-18 09:10:32 +02:00
pass
def check_printer_support(self):
2017-09-18 09:10:32 +02:00
pass
2017-02-06 09:51:11 +01:00
def check_preview_support(self):
2017-09-18 09:10:32 +02:00
pass
2017-02-06 09:51:11 +01:00
def to_pdf(self, filename):
printer = QPrinter()
printer.setOutputFileName(filename)
self.to_printer(printer)
2017-02-06 12:49:02 +01:00
def to_printer(self, printer, callback=None):
self._widget.print(printer)
2017-02-06 09:51:11 +01:00
# Can't find out whether there was an error...
if callback is not None:
callback(True)
2016-07-10 17:23:08 +02:00
class WebKitSearch(browsertab.AbstractSearch):
2016-07-04 11:23:46 +02:00
"""QtWebKit implementations related to searching on the page."""
def __init__(self, parent=None):
super().__init__(parent)
self._flags = QWebPage.FindFlags(0)
def _call_cb(self, callback, found, text, flags, caller):
"""Call the given callback if it's non-None.
Delays the call via a QTimer so the website is re-rendered in between.
Args:
callback: What to call
found: If the text was found
text: The text searched for
flags: The flags searched with
caller: Name of the caller.
"""
found_text = 'found' if found else "didn't find"
# Removing FindWrapsAroundDocument to get the same logging as with
# QtWebEngine
debug_flags = debug.qflags_key(
QWebPage, flags & ~QWebPage.FindWrapsAroundDocument,
klass=QWebPage.FindFlag)
if debug_flags != '0x0000':
flag_text = 'with flags {}'.format(debug_flags)
else:
flag_text = ''
log.webview.debug(' '.join([caller, found_text, text, flag_text])
.strip())
if callback is not None:
QTimer.singleShot(0, functools.partial(callback, found))
2016-07-04 11:23:46 +02:00
def clear(self):
self.search_displayed = False
2016-07-04 11:23:46 +02:00
# We first clear the marked text, then the highlights
self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences)
2016-07-04 11:23:46 +02:00
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
" for {}".format(text))
return
2018-03-02 06:31:23 +01:00
# Clear old search results, this is done automatically on QtWebEngine.
self.clear()
self.text = text
self.search_displayed = True
self._flags = QWebPage.FindWrapsAroundDocument
if self._is_case_sensitive(ignore_case):
self._flags |= QWebPage.FindCaseSensitively
2016-07-04 11:23:46 +02:00
if reverse:
self._flags |= QWebPage.FindBackward
2016-07-04 11:23:46 +02:00
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
found = self._widget.findText(text, self._flags)
self._widget.findText(text,
self._flags | QWebPage.HighlightAllOccurrences)
self._call_cb(result_cb, found, text, self._flags, 'search')
2016-07-04 11:23:46 +02:00
def next_result(self, *, result_cb=None):
self.search_displayed = True
found = self._widget.findText(self.text, self._flags)
self._call_cb(result_cb, found, self.text, self._flags, 'next_result')
2016-07-04 11:23:46 +02:00
def prev_result(self, *, result_cb=None):
self.search_displayed = True
# The int() here makes sure we get a copy of the flags.
flags = QWebPage.FindFlags(int(self._flags))
2016-07-04 11:23:46 +02:00
if flags & QWebPage.FindBackward:
flags &= ~QWebPage.FindBackward
else:
flags |= QWebPage.FindBackward
found = self._widget.findText(self.text, flags)
self._call_cb(result_cb, found, self.text, flags, 'prev_result')
2016-07-04 11:23:46 +02:00
2016-07-10 17:23:08 +02:00
class WebKitCaret(browsertab.AbstractCaret):
2016-06-14 18:08:46 +02:00
"""QtWebKit implementations related to moving the cursor/selection."""
2016-06-14 18:08:46 +02:00
@pyqtSlot(usertypes.KeyMode)
2016-07-07 16:10:35 +02:00
def _on_mode_entered(self, mode):
2016-06-14 18:08:46 +02:00
if mode != usertypes.KeyMode.caret:
return
self.selection_enabled = self._widget.hasSelection()
self.selection_toggled.emit(self.selection_enabled)
settings = self._widget.settings()
2016-06-14 18:08:46 +02:00
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
if self._widget.isVisible():
2016-06-14 18:08:46 +02:00
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self._widget.clearFocus()
self._widget.setFocus(Qt.OtherFocusReason)
2016-06-14 18:08:46 +02:00
# Move the caret to the first element in the viewport if there
# isn't any text which is already selected.
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
2017-12-07 12:45:05 +01:00
if not self.selection_enabled:
self._widget.page().currentFrame().evaluateJavaScript(
2016-06-14 18:08:46 +02:00
utils.read_file('javascript/position_caret.js'))
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, _mode):
settings = self._widget.settings()
2016-06-14 18:08:46 +02:00
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self._widget.hasSelection():
2016-06-14 18:08:46 +02:00
# Remove selection if it exists
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
2016-06-14 18:08:46 +02:00
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False
def move_to_next_line(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToNextLine
else:
act = QWebPage.SelectNextLine
for _ in range(count):
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_prev_line(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousLine
else:
act = QWebPage.SelectPreviousLine
for _ in range(count):
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_next_char(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToNextChar
else:
act = QWebPage.SelectNextChar
for _ in range(count):
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_prev_char(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousChar
else:
act = QWebPage.SelectPreviousChar
for _ in range(count):
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_end_of_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if utils.is_windows: # pragma: no cover
2016-06-14 18:08:46 +02:00
act.append(QWebPage.MoveToPreviousChar)
else:
act = [QWebPage.SelectNextWord]
if utils.is_windows: # pragma: no cover
2016-06-14 18:08:46 +02:00
act.append(QWebPage.SelectPreviousChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
2016-06-14 18:08:46 +02:00
def move_to_next_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if not utils.is_windows: # pragma: no branch
2016-06-14 18:08:46 +02:00
act.append(QWebPage.MoveToNextChar)
else:
act = [QWebPage.SelectNextWord]
if not utils.is_windows: # pragma: no branch
2016-06-14 18:08:46 +02:00
act.append(QWebPage.SelectNextChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
2016-06-14 18:08:46 +02:00
def move_to_prev_word(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousWord
else:
act = QWebPage.SelectPreviousWord
for _ in range(count):
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_start_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfLine
else:
act = QWebPage.SelectStartOfLine
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_end_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfLine
else:
act = QWebPage.SelectEndOfLine
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_start_of_next_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToStartOfBlock]
else:
act = [QWebPage.SelectNextLine,
QWebPage.SelectStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
2016-06-14 18:08:46 +02:00
def move_to_start_of_prev_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToPreviousLine,
QWebPage.MoveToStartOfBlock]
else:
act = [QWebPage.SelectPreviousLine,
QWebPage.SelectStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
2016-06-14 18:08:46 +02:00
def move_to_end_of_next_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToEndOfBlock]
else:
act = [QWebPage.SelectNextLine,
QWebPage.SelectEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
2016-06-14 18:08:46 +02:00
def move_to_end_of_prev_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
else:
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
2016-06-14 18:08:46 +02:00
def move_to_start_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfDocument
else:
act = QWebPage.SelectStartOfDocument
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def move_to_end_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfDocument
else:
act = QWebPage.SelectEndOfDocument
self._widget.triggerPageAction(act)
2016-06-14 18:08:46 +02:00
def toggle_selection(self):
self.selection_enabled = not self.selection_enabled
self.selection_toggled.emit(self.selection_enabled)
2016-06-14 18:08:46 +02:00
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
2016-06-13 14:44:41 +02:00
def selection(self, callback):
callback(self._widget.selectedText())
2016-07-04 13:35:38 +02:00
def follow_selected(self, *, tab=False):
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
if tab:
self._tab.data.override_target = usertypes.ClickTarget.tab
self._tab.run_js_async("""
const aElm = document.activeElement;
if (window.getSelection().anchorNode) {
window.getSelection().anchorNode.parentNode.click();
} else if (aElm && aElm !== document.body) {
aElm.click();
}
""")
2016-07-04 13:35:38 +02:00
else:
selection = self._widget.selectedHtml()
if not selection:
# Getting here may mean we crashed, but we can't do anything
2018-06-11 01:27:56 +02:00
# about that until this commit is released:
# https://github.com/annulen/webkit/commit/0e75f3272d149bc64899c161f150eb341a2417af
# TODO find a way to check if something is focused
2018-06-11 11:32:15 +02:00
self._follow_enter(tab)
return
2016-07-04 13:35:38 +02:00
try:
selected_element = xml.etree.ElementTree.fromstring(
'<html>{}</html>'.format(selection)).find('a')
except xml.etree.ElementTree.ParseError:
2016-07-10 17:33:36 +02:00
raise browsertab.WebTabError('Could not parse selected '
'element!')
2016-07-04 13:35:38 +02:00
if selected_element is not None:
try:
url = selected_element.attrib['href']
except KeyError:
2016-07-10 17:33:36 +02:00
raise browsertab.WebTabError('Anchor element without '
'href!')
url = self._tab.url().resolved(QUrl(url))
2016-07-04 13:51:11 +02:00
if tab:
self._tab.new_tab_requested.emit(url)
else:
self._tab.openurl(url)
2016-07-04 13:35:38 +02:00
2016-06-13 14:44:41 +02:00
2016-07-10 17:23:08 +02:00
class WebKitZoom(browsertab.AbstractZoom):
2016-06-15 13:02:24 +02:00
"""QtWebKit implementations related to zooming."""
2016-06-15 13:02:24 +02:00
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
2016-06-15 13:02:24 +02:00
2016-07-10 17:23:08 +02:00
class WebKitScroller(browsertab.AbstractScroller):
2016-06-14 17:32:36 +02:00
"""QtWebKit implementations related to scrolling."""
2016-07-07 18:03:37 +02:00
# FIXME:qtwebengine When to use the main frame, when the current one?
2016-06-14 17:32:36 +02:00
def pos_px(self):
return self._widget.page().mainFrame().scrollPosition()
2016-06-14 17:32:36 +02:00
def pos_perc(self):
return self._widget.scroll_pos
2016-06-14 17:32:36 +02:00
def to_point(self, point):
self._widget.page().mainFrame().setScrollPosition(point)
2016-06-14 17:32:36 +02:00
def to_anchor(self, name):
self._widget.page().mainFrame().scrollToAnchor(name)
2016-06-15 13:04:50 +02:00
def delta(self, x=0, y=0):
2016-06-14 17:32:36 +02:00
qtutils.check_overflow(x, 'int')
qtutils.check_overflow(y, 'int')
self._widget.page().mainFrame().scroll(x, y)
2016-06-14 17:32:36 +02:00
2016-07-05 20:11:42 +02:00
def delta_page(self, x=0.0, y=0.0):
2016-06-14 17:32:36 +02:00
if y.is_integer():
y = int(y)
if y == 0:
pass
elif y < 0:
2016-06-15 13:04:50 +02:00
self.page_up(count=-y)
2016-06-14 17:32:36 +02:00
elif y > 0:
self.page_down(count=y)
y = 0
if x == 0 and y == 0:
return
size = self._widget.page().mainFrame().geometry()
2016-06-14 17:32:36 +02:00
self.delta(x * size.width(), y * size.height())
def to_perc(self, x=None, y=None):
if x is None and y == 0:
self.top()
elif x is None and y == 100:
self.bottom()
else:
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
2016-06-15 13:04:50 +02:00
if val is not None:
frame = self._widget.page().mainFrame()
maximum = frame.scrollBarMaximum(orientation)
if maximum == 0:
2016-06-15 13:04:50 +02:00
continue
pos = int(maximum * val / 100)
pos = qtutils.check_overflow(pos, 'int', fatal=False)
frame.setScrollBarValue(orientation, pos)
2016-06-14 17:32:36 +02:00
def _key_press(self, key, count=1, getter_name=None, direction=None):
frame = self._widget.page().mainFrame()
2016-06-14 17:32:36 +02:00
getter = None if getter_name is None else getattr(frame, getter_name)
2016-07-07 18:03:37 +02:00
# FIXME:qtwebengine needed?
# self._widget.setFocus()
2017-02-11 22:26:37 +01:00
for _ in range(min(count, 5000)):
2016-06-14 17:32:36 +02:00
# Abort scrolling if the minimum/maximum was reached.
2016-06-15 13:04:50 +02:00
if (getter is not None and
frame.scrollBarValue(direction) == getter(direction)):
2016-06-14 17:32:36 +02:00
return
self._tab.key_press(key)
2016-06-14 17:32:36 +02:00
def up(self, count=1):
self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical)
def down(self, count=1):
self._key_press(Qt.Key_Down, count, 'scrollBarMaximum', Qt.Vertical)
def left(self, count=1):
self._key_press(Qt.Key_Left, count, 'scrollBarMinimum', Qt.Horizontal)
def right(self, count=1):
self._key_press(Qt.Key_Right, count, 'scrollBarMaximum', Qt.Horizontal)
def top(self):
self._key_press(Qt.Key_Home)
def bottom(self):
self._key_press(Qt.Key_End)
def page_up(self, count=1):
self._key_press(Qt.Key_PageUp, count, 'scrollBarMinimum', Qt.Vertical)
def page_down(self, count=1):
self._key_press(Qt.Key_PageDown, count, 'scrollBarMaximum',
Qt.Vertical)
def at_top(self):
return self.pos_px().y() == 0
def at_bottom(self):
frame = self._widget.page().currentFrame()
2016-06-14 17:32:36 +02:00
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
2016-07-10 17:23:08 +02:00
class WebKitHistory(browsertab.AbstractHistory):
2016-06-14 11:03:34 +02:00
"""QtWebKit implementations related to page history."""
2016-06-14 13:53:35 +02:00
def current_idx(self):
2016-07-04 12:50:15 +02:00
return self._history.currentItemIndex()
2016-06-14 13:53:35 +02:00
2016-06-14 11:03:34 +02:00
def can_go_back(self):
2016-07-04 12:50:15 +02:00
return self._history.canGoBack()
2016-06-14 11:03:34 +02:00
def can_go_forward(self):
2016-07-04 12:50:15 +02:00
return self._history.canGoForward()
2016-06-14 11:03:34 +02:00
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
2016-06-14 11:03:34 +02:00
def serialize(self):
2016-07-04 12:50:15 +02:00
return qtutils.serialize(self._history)
2016-06-14 11:03:34 +02:00
def deserialize(self, data):
2016-07-04 12:50:15 +02:00
return qtutils.deserialize(data, self._history)
2016-06-14 11:03:34 +02:00
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
2016-06-14 11:03:34 +02:00
stream, _data, user_data = tabhistory.serialize(items)
2016-07-04 12:50:15 +02:00
qtutils.deserialize_stream(stream, self._history)
2016-06-14 11:03:34 +02:00
for i, data in enumerate(user_data):
2016-07-04 12:50:15 +02:00
self._history.itemAt(i).setUserData(data)
cur_data = self._history.currentItem().userData()
2016-06-14 11:03:34 +02:00
if cur_data is not None:
if 'zoom' in cur_data:
2016-06-15 14:23:24 +02:00
self._tab.zoom.set_factor(cur_data['zoom'])
2016-06-14 11:03:34 +02:00
if ('scroll-pos' in cur_data and
self._tab.scroller.pos_px() == QPoint(0, 0)):
2016-06-14 11:03:34 +02:00
QTimer.singleShot(0, functools.partial(
self._tab.scroller.to_point, cur_data['scroll-pos']))
2016-06-14 11:03:34 +02:00
2016-08-18 14:01:27 +02:00
class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implemementations related to elements on the page."""
def find_css(self, selector, callback, *, only_visible=False):
mainframe = self._widget.page().mainFrame()
if mainframe is None:
raise browsertab.WebTabError("No frame focused!")
elems = []
frames = webkitelem.get_child_frames(mainframe)
for f in frames:
for elem in f.findAllElements(selector):
elems.append(webkitelem.WebKitElement(elem, tab=self._tab))
2016-08-18 14:01:27 +02:00
if only_visible:
# pylint: disable=protected-access
elems = [e for e in elems if e._is_visible(mainframe)]
# pylint: enable=protected-access
2016-08-18 14:01:27 +02:00
callback(elems)
2016-08-18 14:08:34 +02:00
def find_id(self, elem_id, callback):
2016-08-18 15:30:04 +02:00
def find_id_cb(elems):
2017-12-15 13:55:06 +01:00
"""Call the real callback with the found elements."""
2016-08-18 15:30:04 +02:00
if not elems:
callback(None)
else:
callback(elems[0])
# Escape non-alphanumeric characters in the selector
# https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id)
2016-08-18 15:30:04 +02:00
self.find_css('#' + elem_id, find_id_cb)
2016-08-18 14:08:34 +02:00
2016-08-18 14:01:27 +02:00
def find_focused(self, callback):
frame = self._widget.page().currentFrame()
if frame is None:
callback(None)
return
elem = frame.findFirstElement('*:focus')
if elem.isNull():
callback(None)
else:
callback(webkitelem.WebKitElement(elem, tab=self._tab))
2016-08-18 14:01:27 +02:00
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
frame = self._widget.page().frameAt(pos)
if frame is None:
# This happens when we click inside the webview, but not actually
# on the QWebPage - for example when clicking the scrollbar
# sometimes.
log.webview.debug("Hit test at {} but frame is None!".format(pos))
callback(None)
return
# You'd think we have to subtract frame.geometry().topLeft() from the
# position, but it seems QWebFrame::hitTestContent wants a position
# relative to the QWebView, not to the frame. This makes no sense to
# me, but it works this way.
hitresult = frame.hitTestContent(pos)
if hitresult.isNull():
# For some reason, the whole hit result can be null sometimes (e.g.
2016-09-07 11:24:28 +02:00
# on doodle menu links).
2016-08-18 14:01:27 +02:00
log.webview.debug("Hit test result is null!")
callback(None)
return
try:
elem = webkitelem.WebKitElement(hitresult.element(), tab=self._tab)
2016-08-18 14:01:27 +02:00
except webkitelem.IsNullError:
# For some reason, the hit result element can be a null element
# sometimes (e.g. when clicking the timetable fields on
2016-09-07 11:24:28 +02:00
# http://www.sbb.ch/ ).
2016-08-18 14:01:27 +02:00
log.webview.debug("Hit test result element is null!")
callback(None)
return
callback(elem)
class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit."""
def set_muted(self, muted: bool):
raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self):
return False
def is_recently_audible(self):
return False
2016-07-10 17:23:08 +02:00
class WebKitTab(browsertab.AbstractTab):
2016-06-13 14:44:41 +02:00
"""A QtWebKit tab in the browser."""
2017-04-24 21:08:00 +02:00
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager,
2017-04-24 21:08:00 +02:00
private=private, parent=parent)
widget = webview.WebView(win_id=win_id, tab_id=self.tab_id,
private=private, tab=self)
if private:
self._make_private(widget)
2016-07-10 17:23:08 +02:00
self.history = WebKitHistory(self)
self.scroller = WebKitScroller(self, parent=self)
self.caret = WebKitCaret(mode_manager=mode_manager,
2016-07-10 17:23:08 +02:00
tab=self, parent=self)
self.zoom = WebKitZoom(tab=self, parent=self)
2016-07-10 17:23:08 +02:00
self.search = WebKitSearch(parent=self)
self.printing = WebKitPrinting()
self.elements = WebKitElements(tab=self)
self.action = WebKitAction(tab=self)
self.audio = WebKitAudio()
# We're assigning settings in _set_widget
self.settings = webkitsettings.WebKitSettings(settings=None)
2016-06-13 15:05:31 +02:00
self._set_widget(widget)
2016-06-13 14:44:41 +02:00
self._connect_signals()
self.backend = usertypes.Backend.QtWebKit
2016-06-13 14:44:41 +02:00
def _install_event_filter(self):
self._widget.installEventFilter(self._mouse_event_filter)
2016-08-10 16:37:52 +02:00
2017-04-24 21:08:00 +02:00
def _make_private(self, widget):
settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
2018-03-16 09:07:25 +01:00
def openurl(self, url, *, predict=True):
self._openurl_prepare(url, predict=predict)
2016-06-13 15:05:31 +02:00
self._widget.openurl(url)
def url(self, requested=False):
frame = self._widget.page().mainFrame()
if requested:
return frame.requestedUrl()
else:
return frame.url()
2016-06-13 15:05:31 +02:00
2016-06-14 18:35:28 +02:00
def dump_async(self, callback, *, plain=False):
2016-06-14 13:26:30 +02:00
frame = self._widget.page().mainFrame()
if plain:
callback(frame.toPlainText())
else:
callback(frame.toHtml())
def run_js_async(self, code, callback=None, *, world=None):
2016-09-12 18:27:51 +02:00
if world is not None and world != usertypes.JsWorld.jseval:
log.webview.warning("Ignoring world ID {}".format(world))
2016-10-30 18:49:41 +01:00
document_element = self._widget.page().mainFrame().documentElement()
result = document_element.evaluateJavaScript(code)
2016-06-14 18:47:26 +02:00
if callback is not None:
callback(result)
2016-06-14 15:22:22 +02:00
def icon(self):
return self._widget.icon()
2016-06-14 13:31:02 +02:00
def shutdown(self):
self._widget.shutdown()
2016-06-14 13:39:51 +02:00
def reload(self, *, force=False):
if force:
action = QWebPage.ReloadAndBypassCache
else:
action = QWebPage.Reload
self._widget.triggerPageAction(action)
2016-06-14 13:39:51 +02:00
def stop(self):
self._widget.stop()
2016-06-14 13:53:35 +02:00
def title(self):
return self._widget.title()
def clear_ssl_errors(self):
2016-11-22 11:23:45 +01:00
self.networkaccessmanager().clear_all_ssl_errors()
def key_press(self, key, modifier=Qt.NoModifier):
press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
@pyqtSlot()
def _on_history_trigger(self):
url = self.url()
requested_url = self.url(requested=True)
self.add_history_item.emit(url, requested_url, self.title())
def set_html(self, html, base_url=QUrl()):
2016-07-04 16:33:49 +02:00
self._widget.setHtml(html, base_url)
2016-11-22 11:23:45 +01:00
def networkaccessmanager(self):
return self._widget.page().networkAccessManager()
def user_agent(self):
page = self._widget.page()
return page.userAgentForUrl(self.url())
@pyqtSlot()
def _on_load_started(self):
super()._on_load_started()
2018-02-11 10:29:02 +01:00
self.networkaccessmanager().netrc_used = False
# Make sure the icon is cleared when navigating to a page without one.
self.icon_changed.emit(QIcon())
@pyqtSlot()
def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished.
While Qt has a bool "ok" attribute for loadFinished, it always is True
when using error pages... See
2017-02-05 00:13:11 +01:00
https://github.com/qutebrowser/qutebrowser/issues/84
"""
self._on_load_finished(not self._widget.page().error_occurred)
@pyqtSlot()
def _on_webkit_icon_changed(self):
"""Emit iconChanged with a QIcon like QWebEngineView does."""
if sip.isdeleted(self._widget):
log.webview.debug("Got _on_webkit_icon_changed for deleted view!")
return
self.icon_changed.emit(self._widget.icon())
2016-07-27 15:34:28 +02:00
@pyqtSlot(QWebFrame)
def _on_frame_created(self, frame):
"""Connect the contentsSizeChanged signal of each frame."""
# FIXME:qtwebengine those could theoretically regress:
2017-02-05 00:13:11 +01:00
# https://github.com/qutebrowser/qutebrowser/issues/152
# https://github.com/qutebrowser/qutebrowser/issues/263
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
2016-07-27 15:34:28 +02:00
@pyqtSlot(QSize)
def _on_contents_size_changed(self, size):
self.contents_size_changed.emit(QSizeF(size))
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
2018-02-20 12:25:59 +01:00
if not navigation.accepted:
return
log.webview.debug("target {} override {}".format(
self.data.open_target, self.data.override_target))
if self.data.override_target is not None:
target = self.data.override_target
self.data.override_target = None
else:
target = self.data.open_target
if (navigation.navigation_type == navigation.Type.link_clicked and
target != usertypes.ClickTarget.normal):
tab = shared.get_tab(self.win_id, target)
tab.openurl(navigation.url)
self.data.open_target = usertypes.ClickTarget.normal
navigation.accepted = False
if navigation.is_main_frame:
self.settings.update_for_url(navigation.url)
2016-06-13 14:44:41 +02:00
def _connect_signals(self):
view = self._widget
page = view.page()
frame = page.mainFrame()
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
2016-07-04 16:34:07 +02:00
frame.loadStarted.connect(self._on_load_started)
view.scroll_pos_changed.connect(self.scroller.perc_changed)
2016-06-13 14:44:41 +02:00
view.titleChanged.connect(self.title_changed)
2016-07-11 16:05:09 +02:00
view.urlChanged.connect(self._on_url_changed)
2016-06-14 13:31:02 +02:00
view.shutting_down.connect(self.shutting_down)
page.networkAccessManager().sslErrors.connect(self._on_ssl_errors)
frame.loadFinished.connect(self._on_frame_load_finished)
view.iconChanged.connect(self._on_webkit_icon_changed)
page.frameCreated.connect(self._on_frame_created)
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
2016-08-10 13:14:38 +02:00
frame.initialLayoutCompleted.connect(self._on_history_trigger)
page.navigation_request.connect(self._on_navigation_request)
2016-08-17 18:02:24 +02:00
def event_target(self):
return self._widget