Merge branch 'qtwebengine'

This commit is contained in:
Florian Bruhin 2016-07-11 10:56:20 +02:00
commit 8567fffdad
52 changed files with 2545 additions and 1054 deletions

View File

@ -14,6 +14,7 @@ disable=no-self-use,
fixme,
global-statement,
locally-disabled,
locally-enabled,
too-many-ancestors,
too-few-public-methods,
too-many-public-methods,
@ -32,12 +33,13 @@ disable=no-self-use,
ungrouped-imports,
redefined-variable-type,
suppressed-message,
too-many-return-statements
too-many-return-statements,
duplicate-code
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
method-rgx=[a-z_][A-Za-z0-9_]{2,50}$
method-rgx=[a-z_][A-Za-z0-9_]{1,50}$
attr-rgx=[a-z_][a-z0-9_]{0,30}$
argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$

View File

@ -0,0 +1,544 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
"""Base class for a wrapper over QWebView/QWebEngineView."""
import itertools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QLayout
from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, usertypes, message
tab_id_gen = itertools.count(0)
def create(win_id, parent=None):
"""Get a QtWebKit/QtWebEngine tab object.
Args:
win_id: The window ID where the tab will be shown.
parent: The Qt parent to set.
"""
# Importing modules here so we don't depend on QtWebEngine without the
# argument and to avoid circular imports.
mode_manager = modeman.instance(win_id)
if objreg.get('args').backend == 'webengine':
from qutebrowser.browser.webengine import webenginetab
tab_class = webenginetab.WebEngineTab
else:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent)
class WebTabError(Exception):
"""Base class for various errors."""
class WrapperLayout(QLayout):
"""A Qt layout which simply wraps a single widget.
This is used so the widget is hidden behind a AbstractTab API and can't
easily be accidentally accessed.
"""
def __init__(self, widget, parent=None):
super().__init__(parent)
self._widget = widget
def addItem(self, _widget):
raise AssertionError("Should never be called!")
def sizeHint(self):
return self._widget.sizeHint()
def itemAt(self, _index): # pragma: no cover
# For some reason this sometimes gets called by Qt.
return None
def takeAt(self, _index):
raise AssertionError("Should never be called!")
def setGeometry(self, rect):
self._widget.setGeometry(rect)
class TabData:
"""A simple namespace with a fixed set of attributes.
Attributes:
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
inspector: The QWebInspector used for this webview.
viewing_source: Set if we're currently showing a source view.
"""
__slots__ = ['keep_icon', 'viewing_source', 'inspector']
def __init__(self):
self.keep_icon = False
self.viewing_source = False
self.inspector = None
class AbstractSearch(QObject):
"""Attribute of AbstractTab for doing searches.
Attributes:
text: The last thing this view was searched for.
_flags: The flags of the last search.
_widget: The underlying WebView widget.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._widget = None
self.text = None
self._flags = 0
def search(self, text, *, ignore_case=False, wrap=False, reverse=False):
"""Find the given text on the page.
Args:
text: The text to search for.
ignore_case: Search case-insensitively. (True/False/'smart')
wrap: Wrap around to the top when arriving at the bottom.
reverse: Reverse search direction.
"""
raise NotImplementedError
def clear(self):
"""Clear the current search."""
raise NotImplementedError
def prev_result(self):
"""Go to the previous result of the current search."""
raise NotImplementedError
def next_result(self):
"""Go to the next result of the current search."""
raise NotImplementedError
class AbstractZoom(QObject):
"""Attribute of AbstractTab for controlling zoom.
Attributes:
_neighborlist: A NeighborList with the zoom levels.
_default_zoom_changed: Whether the zoom was changed from the default.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._widget = None
self._win_id = win_id
self._default_zoom_changed = False
self._init_neighborlist()
objreg.get('config').changed.connect(self._on_config_changed)
# # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically
# # when the WebView is destroyed on older PyQt versions.
# # See https://github.com/The-Compiler/qutebrowser/issues/390
# self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist))
@pyqtSlot(str, str)
def _on_config_changed(self, section, option):
if section == 'ui' and option in ('zoom-levels', 'default-zoom'):
if not self._default_zoom_changed:
factor = float(config.get('ui', 'default-zoom')) / 100
self._set_factor_internal(factor)
self._default_zoom_changed = False
self._init_neighborlist()
def _init_neighborlist(self):
"""Initialize self._neighborlist."""
levels = config.get('ui', 'zoom-levels')
self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.get('ui', 'default-zoom')
def offset(self, offset):
"""Increase/Decrease the zoom level by the given offset.
Args:
offset: The offset in the zoom level list.
Return:
The new zoom percentage.
"""
level = self._neighborlist.getitem(offset)
self.set_factor(float(level) / 100, fuzzyval=False)
return level
def set_factor(self, factor, *, fuzzyval=True):
"""Zoom to a given zoom factor.
Args:
factor: The zoom factor as float.
fuzzyval: Whether to set the NeighborLists fuzzyval.
"""
if fuzzyval:
self._neighborlist.fuzzyval = int(factor * 100)
if factor < 0:
raise ValueError("Can't zoom to factor {}!".format(factor))
self._default_zoom_changed = True
self._set_factor_internal(factor)
def factor(self):
raise NotImplementedError
def set_default(self):
default_zoom = config.get('ui', 'default-zoom')
self._set_factor_internal(float(default_zoom) / 100)
@pyqtSlot(QPoint)
def _on_mouse_wheel_zoom(self, delta):
"""Handle zooming via mousewheel requested by the web view."""
divider = config.get('input', 'mouse-zoom-divider')
factor = self.factor() + delta.y() / divider
if factor < 0:
return
perc = int(100 * factor)
message.info(self._win_id, "Zoom level: {}%".format(perc))
self._neighborlist.fuzzyval = perc
self._set_factor_internal(factor)
self._default_zoom_changed = True
class AbstractCaret(QObject):
"""Attribute of AbstractTab for caret browsing."""
def __init__(self, win_id, tab, mode_manager, parent=None):
super().__init__(parent)
self._tab = tab
self._win_id = win_id
self._widget = None
self.selection_enabled = False
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
def _on_mode_entered(self, mode):
raise NotImplementedError
def _on_mode_left(self):
raise NotImplementedError
def move_to_next_line(self, count=1):
raise NotImplementedError
def move_to_prev_line(self, count=1):
raise NotImplementedError
def move_to_next_char(self, count=1):
raise NotImplementedError
def move_to_prev_char(self, count=1):
raise NotImplementedError
def move_to_end_of_word(self, count=1):
raise NotImplementedError
def move_to_next_word(self, count=1):
raise NotImplementedError
def move_to_prev_word(self, count=1):
raise NotImplementedError
def move_to_start_of_line(self):
raise NotImplementedError
def move_to_end_of_line(self):
raise NotImplementedError
def move_to_start_of_next_block(self, count=1):
raise NotImplementedError
def move_to_start_of_prev_block(self, count=1):
raise NotImplementedError
def move_to_end_of_next_block(self, count=1):
raise NotImplementedError
def move_to_end_of_prev_block(self, count=1):
raise NotImplementedError
def move_to_start_of_document(self):
raise NotImplementedError
def move_to_end_of_document(self):
raise NotImplementedError
def toggle_selection(self):
raise NotImplementedError
def drop_selection(self):
raise NotImplementedError
def has_selection(self):
raise NotImplementedError
def selection(self, html=False):
raise NotImplementedError
def follow_selected(self, *, tab=False):
raise NotImplementedError
class AbstractScroller(QObject):
"""Attribute of AbstractTab to manage scroll position."""
perc_changed = pyqtSignal(int, int)
def __init__(self, parent=None):
super().__init__(parent)
self._widget = None
def pos_px(self):
raise NotImplementedError
def pos_perc(self):
raise NotImplementedError
def to_perc(self, x=None, y=None):
raise NotImplementedError
def to_point(self, point):
raise NotImplementedError
def delta(self, x=0, y=0):
raise NotImplementedError
def delta_page(self, x=0, y=0):
raise NotImplementedError
def up(self, count=1):
raise NotImplementedError
def down(self, count=1):
raise NotImplementedError
def left(self, count=1):
raise NotImplementedError
def right(self, count=1):
raise NotImplementedError
def top(self):
raise NotImplementedError
def bottom(self):
raise NotImplementedError
def page_up(self, count=1):
raise NotImplementedError
def page_down(self, count=1):
raise NotImplementedError
def at_top(self):
raise NotImplementedError
def at_bottom(self):
raise NotImplementedError
class AbstractHistory:
"""The history attribute of a AbstractTab."""
def __init__(self, tab):
self._tab = tab
self._history = None
def __len__(self):
return len(self._history)
def __iter__(self):
return iter(self._history.items())
def current_idx(self):
raise NotImplementedError
def back(self):
raise NotImplementedError
def forward(self):
raise NotImplementedError
def can_go_back(self):
raise NotImplementedError
def can_go_forward(self):
raise NotImplementedError
def serialize(self):
"""Serialize into an opaque format understood by self.deserialize."""
raise NotImplementedError
def deserialize(self, data):
"""Serialize from a format produced by self.serialize."""
raise NotImplementedError
def load_items(self, items):
"""Deserialize from a list of WebHistoryItems."""
raise NotImplementedError
class AbstractTab(QWidget):
"""A wrapper over the given widget to hide its API and expose another one.
We use this to unify QWebView and QWebEngineView.
Attributes:
history: The AbstractHistory for the current tab.
registry: The ObjectRegistry associated with this tab.
for properties, see WebView/WebEngineView docs.
Signals:
See related Qt signals.
new_tab_requested: Emitted when a new tab should be opened with the
given URL.
"""
window_close_requested = pyqtSignal()
link_hovered = pyqtSignal(str)
load_started = pyqtSignal()
load_progress = pyqtSignal(int)
load_finished = pyqtSignal(bool)
icon_changed = pyqtSignal(QIcon)
# FIXME:qtwebengine get rid of this altogether?
url_text_changed = pyqtSignal(str)
title_changed = pyqtSignal(str)
load_status_changed = pyqtSignal(str)
new_tab_requested = pyqtSignal(QUrl)
shutting_down = pyqtSignal()
def __init__(self, win_id, parent=None):
self.win_id = win_id
self.tab_id = next(tab_id_gen)
super().__init__(parent)
self.registry = objreg.ObjectRegistry()
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('tab', self, registry=self.registry)
# self.history = AbstractHistory(self)
# self.scroll = AbstractScroller(parent=self)
# self.caret = AbstractCaret(win_id=win_id, tab=self, mode_manager=...,
# parent=self)
# self.zoom = AbstractZoom(win_id=win_id)
# self.search = AbstractSearch(parent=self)
self.data = TabData()
self._layout = None
self._widget = None
self.backend = None
def _set_widget(self, widget):
# pylint: disable=protected-access
self._layout = WrapperLayout(widget, self)
self._widget = widget
self.history._history = widget.history()
self.scroll._widget = widget
self.caret._widget = widget
self.zoom._widget = widget
self.search._widget = widget
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
widget.setParent(self)
self.setFocusProxy(widget)
@pyqtSlot()
def _on_load_started(self):
self.data.viewing_source = False
self.load_started.emit()
def url(self):
raise NotImplementedError
def progress(self):
raise NotImplementedError
def load_status(self):
raise NotImplementedError
def openurl(self, url):
raise NotImplementedError
def reload(self, *, force=False):
raise NotImplementedError
def stop(self):
raise NotImplementedError
def clear_ssl_errors(self):
raise NotImplementedError
def dump_async(self, callback, *, plain=False):
"""Dump the current page to a file ascync.
The given callback will be called with the result when dumping is
complete.
"""
raise NotImplementedError
def run_js_async(self, code, callback=None):
"""Run javascript async.
The given callback will be called with the result when running JS is
complete.
"""
raise NotImplementedError
def shutdown(self):
raise NotImplementedError
def title(self):
raise NotImplementedError
def icon(self):
raise NotImplementedError
def set_html(self, html, base_url):
raise NotImplementedError
def __repr__(self):
try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),
100)
except AttributeError:
url = '<AttributeError>'
return utils.get_repr(self, tab_id=self.tab_id, url=url)

File diff suppressed because it is too large Load Diff

View File

@ -84,6 +84,7 @@ class HintContext:
args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting.
mainframe: The main QWebFrame where we started hinting in.
tab: The WebTab object we started hinting in.
group: The group of web elements to hint.
"""
@ -98,6 +99,7 @@ class HintContext:
self.destroyed_frames = []
self.args = []
self.mainframe = None
self.tab = None
self.group = None
def get_args(self, urlstr):
@ -569,7 +571,6 @@ class HintManager(QObject):
"""
cmd = context.args[0]
args = context.args[1:]
frame = context.mainframe
env = {
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
@ -578,8 +579,12 @@ class HintManager(QObject):
url = self._resolve_url(elem, context.baseurl)
if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(frame))
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
try:
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
env=env)
except userscripts.UnsupportedError as e:
message.error(self._win_id, str(e), immediately=True)
def _spawn(self, url, context):
"""Spawn a simple command from a hint.
@ -752,12 +757,13 @@ class HintManager(QObject):
window=self._win_id)
tabbed_browser.tabopen(url, background=background)
else:
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.openurl(url)
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
tab.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2)
star_args_optional=True, maxsplit=2,
backend=usertypes.Backend.QtWebKit)
@cmdutils.argument('win_id', win_id=True)
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args, win_id):
@ -811,10 +817,12 @@ class HintManager(QObject):
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
widget = tabbed_browser.currentWidget()
if widget is None:
tab = tabbed_browser.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")
mainframe = widget.page().mainFrame()
# FIXME:qtwebengine have a proper API for this
page = tab._widget.page() # pylint: disable=protected-access
mainframe = page.mainFrame()
if mainframe is None:
raise cmdexc.CommandError("No frame focused!")
mode_manager = objreg.get('mode-manager', scope='window',
@ -837,6 +845,7 @@ class HintManager(QObject):
self._check_args(target, *args)
self._context = HintContext()
self._context.tab = tab
self._context.target = target
self._context.rapid = rapid
try:

View File

@ -0,0 +1,20 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
"""Classes related to the browser widgets for QtWebEngine."""

View File

@ -0,0 +1,332 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
# FIXME:qtwebengine remove this once the stubs are gone
# pylint: disable=unused-variable
"""Wrapper over a QWebEngineView."""
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QApplication
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEnginePage
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import browsertab
from qutebrowser.browser.webengine import webview
from qutebrowser.utils import usertypes, qtutils, log
class WebEngineSearch(browsertab.AbstractSearch):
"""QtWebEngine implementations related to searching on the page."""
def search(self, text, *, ignore_case=False, wrap=False, reverse=False):
log.stub()
def clear(self):
log.stub()
def prev_result(self):
log.stub()
def next_result(self):
log.stub()
class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection."""
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
log.stub()
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self):
log.stub()
def move_to_next_line(self, count=1):
log.stub()
def move_to_prev_line(self, count=1):
log.stub()
def move_to_next_char(self, count=1):
log.stub()
def move_to_prev_char(self, count=1):
log.stub()
def move_to_end_of_word(self, count=1):
log.stub()
def move_to_next_word(self, count=1):
log.stub()
def move_to_prev_word(self, count=1):
log.stub()
def move_to_start_of_line(self):
log.stub()
def move_to_end_of_line(self):
log.stub()
def move_to_start_of_next_block(self, count=1):
log.stub()
def move_to_start_of_prev_block(self, count=1):
log.stub()
def move_to_end_of_next_block(self, count=1):
log.stub()
def move_to_end_of_prev_block(self, count=1):
log.stub()
def move_to_start_of_document(self):
log.stub()
def move_to_end_of_document(self):
log.stub()
def toggle_selection(self):
log.stub()
def drop_selection(self):
log.stub()
def has_selection(self):
return self._widget.hasSelection()
def selection(self, html=False):
if html:
raise NotImplementedError
return self._widget.selectedText()
def follow_selected(self, *, tab=False):
log.stub()
class WebEngineScroller(browsertab.AbstractScroller):
"""QtWebEngine implementations related to scrolling."""
def _key_press(self, key, count=1):
# FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached.
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
recipient = self._widget.focusProxy()
for _ in range(count):
# If we get a segfault here, we might want to try sendEvent
# instead.
QApplication.postEvent(recipient, press_evt)
QApplication.postEvent(recipient, release_evt)
def pos_px(self):
log.stub()
return QPoint(0, 0)
def pos_perc(self):
page = self._widget.page()
try:
size = page.contentsSize()
pos = page.scrollPosition()
except AttributeError:
# Added in Qt 5.7
log.stub('on Qt < 5.7')
return (None, None)
else:
# FIXME:qtwebengine is this correct?
perc_x = 100 / size.width() * pos.x()
perc_y = 100 / size.height() * pos.y()
return (perc_x, perc_y)
def to_perc(self, x=None, y=None):
log.stub()
def to_point(self, point):
log.stub()
def delta(self, x=0, y=0):
log.stub()
def delta_page(self, x=0, y=0):
log.stub()
def up(self, count=1):
self._key_press(Qt.Key_Up, count)
def down(self, count=1):
self._key_press(Qt.Key_Down, count)
def left(self, count=1):
self._key_press(Qt.Key_Left, count)
def right(self, count=1):
self._key_press(Qt.Key_Right, count)
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)
def page_down(self, count=1):
self._key_press(Qt.Key_PageDown, count)
def at_top(self):
log.stub()
def at_bottom(self):
log.stub()
class WebEngineHistory(browsertab.AbstractHistory):
"""QtWebEngine implementations related to page history."""
def current_idx(self):
return self._history.currentItemIndex()
def back(self):
self._history.back()
def forward(self):
self._history.forward()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def serialize(self):
return qtutils.serialize(self._history)
def deserialize(self, data):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
log.stub()
class WebEngineZoom(browsertab.AbstractZoom):
"""QtWebEngine implementations related to zooming."""
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
def factor(self):
return self._widget.zoomFactor()
class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser."""
def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id)
widget = webview.WebEngineView()
self.history = WebEngineHistory(self)
self.scroll = WebEngineScroller()
self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = WebEngineZoom(win_id=win_id, parent=self)
self.search = WebEngineSearch(parent=self)
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
def openurl(self, url):
self._widget.load(url)
def url(self):
return self._widget.url()
def progress(self):
log.stub()
return 0
def load_status(self):
log.stub()
return usertypes.LoadStatus.success
def dump_async(self, callback, *, plain=False):
if plain:
self._widget.page().toPlainText(callback)
else:
self._widget.page().toHtml(callback)
def run_js_async(self, code, callback=None):
if callback is None:
self._widget.page().runJavaScript(code)
else:
self._widget.page().runJavaScript(code, callback)
def shutdown(self):
log.stub()
def reload(self, *, force=False):
if force:
action = QWebEnginePage.ReloadAndBypassCache
else:
action = QWebEnginePage.Reload
self._widget.triggerPageAction(action)
def stop(self):
self._widget.stop()
def title(self):
return self._widget.title()
def icon(self):
return self._widget.icon()
def set_html(self, html, base_url):
# FIXME:qtwebengine
# check this and raise an exception if too big:
# Warning: The content will be percent encoded before being sent to the
# renderer via IPC. This may increase its size. The maximum size of the
# percent encoded content is 2 megabytes minus 30 bytes.
self._widget.setHtml(html, base_url)
def clear_ssl_errors(self):
log.stub()
def _connect_signals(self):
view = self._widget
page = view.page()
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self.load_progress)
page.loadStarted.connect(self._on_load_started)
view.titleChanged.connect(self.title_changed)
page.loadFinished.connect(self.load_finished)
# FIXME:qtwebengine stub this?
# view.iconChanged.connect(self.icon_changed)
# view.scroll.pos_changed.connect(self.scroll.perc_changed)
# view.url_text_changed.connect(self.url_text_changed)
# view.load_status_changed.connect(self.load_status_changed)

View File

@ -0,0 +1,45 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
"""The main browser widget for QtWebEngine."""
from PyQt5.QtCore import pyqtSignal, Qt, QPoint
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEngineView
# pylint: enable=no-name-in-module,import-error,useless-suppression
class WebEngineView(QWebEngineView):
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
mouse_wheel_zoom = pyqtSignal(QPoint)
def wheelEvent(self, e):
"""Zoom on Ctrl-Mousewheel.
Args:
e: The QWheelEvent.
"""
if e.modifiers() & Qt.ControlModifier:
e.accept()
self.mouse_wheel_zoom.emit(e.angleDelta())
else:
super().wheelEvent(e)

View File

@ -222,7 +222,7 @@ class _Downloader:
"""A class to download whole websites.
Attributes:
web_view: The QWebView which contains the website that will be saved.
tab: The AbstractTab which contains the website that will be saved.
dest: Destination filename.
writer: The MHTMLWriter object which is used to save the page.
loaded_urls: A set of QUrls of finished asset downloads.
@ -233,15 +233,15 @@ class _Downloader:
_win_id: The window this downloader belongs to.
"""
def __init__(self, web_view, dest):
self.web_view = web_view
def __init__(self, tab, dest):
self.tab = tab
self.dest = dest
self.writer = None
self.loaded_urls = {web_view.url()}
self.loaded_urls = {tab.url()}
self.pending_downloads = set()
self._finished_file = False
self._used = False
self._win_id = web_view.win_id
self._win_id = tab.win_id
def run(self):
"""Download and save the page.
@ -252,8 +252,11 @@ class _Downloader:
if self._used:
raise ValueError("Downloader already used")
self._used = True
web_url = self.web_view.url()
web_frame = self.web_view.page().mainFrame()
web_url = self.tab.url()
# FIXME:qtwebengine have a proper API for this
page = self.tab._widget.page() # pylint: disable=protected-access
web_frame = page.mainFrame()
self.writer = MHTMLWriter(
web_frame.toHtml().encode('utf-8'),
@ -479,28 +482,28 @@ class _NoCloseBytesIO(io.BytesIO):
super().close()
def _start_download(dest, web_view):
def _start_download(dest, tab):
"""Start downloading the current page and all assets to an MHTML file.
This will overwrite dest if it already exists.
Args:
dest: The filename where the resulting file should be saved.
web_view: Specify the webview whose page should be loaded.
tab: Specify the tab whose page should be loaded.
"""
loader = _Downloader(web_view, dest)
loader = _Downloader(tab, dest)
loader.run()
def start_download_checked(dest, web_view):
def start_download_checked(dest, tab):
"""First check if dest is already a file, then start the download.
Args:
dest: The filename where the resulting file should be saved.
web_view: Specify the webview whose page should be loaded.
tab: Specify the tab whose page should be loaded.
"""
# The default name is 'page title.mht'
title = web_view.title()
title = tab.title()
default_name = utils.sanitize_filename(title + '.mht')
# Remove characters which cannot be expressed in the file system encoding
@ -524,12 +527,12 @@ def start_download_checked(dest, web_view):
# saving the file anyway.
if not os.path.isdir(os.path.dirname(path)):
folder = os.path.dirname(path)
message.error(web_view.win_id,
message.error(tab.win_id,
"Directory {} does not exist.".format(folder))
return
if not os.path.isfile(path):
_start_download(path, web_view=web_view)
_start_download(path, tab=tab)
return
q = usertypes.Question()
@ -537,7 +540,7 @@ def start_download_checked(dest, web_view):
q.text = "{} exists. Overwrite?".format(path)
q.completed.connect(q.deleteLater)
q.answered_yes.connect(functools.partial(
_start_download, path, web_view=web_view))
_start_download, path, tab=tab))
message_bridge = objreg.get('message-bridge', scope='window',
window=web_view.win_id)
window=tab.win_id)
message_bridge.ask(q, blocking=False)

View File

@ -228,9 +228,9 @@ class NetworkManager(QNetworkAccessManager):
# This might be a generic network manager, e.g. one belonging to a
# DownloadManager. In this case, just skip the webview thing.
if self._tab_id is not None:
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.loadStarted.connect(q.abort)
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
tab.load_started.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge.ask(q, blocking=True)
@ -479,9 +479,9 @@ class NetworkManager(QNetworkAccessManager):
if self._tab_id is not None:
try:
webview = objreg.get('webview', scope='tab',
window=self._win_id, tab=self._tab_id)
current_url = webview.url()
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
current_url = tab.url()
except (KeyError, RuntimeError, TypeError):
# https://github.com/The-Compiler/qutebrowser/issues/889
# Catching RuntimeError and TypeError because we could be in

View File

@ -0,0 +1,543 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 sys
import functools
import xml.etree.ElementTree
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWebKitWidgets import QWebPage
from PyQt5.QtWebKit import QWebSettings
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory
from qutebrowser.utils import qtutils, objreg, usertypes, utils
class WebKitSearch(browsertab.AbstractSearch):
"""QtWebKit implementations related to searching on the page."""
def clear(self):
# We first clear the marked text, then the highlights
self._widget.search('', 0)
self._widget.search('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=False, wrap=False, reverse=False):
flags = 0
if ignore_case == 'smart':
if not text.islower():
flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
if wrap:
flags |= QWebPage.FindWrapsAroundDocument
if reverse:
flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
self._widget.search(text, flags)
self._widget.search(text, flags | QWebPage.HighlightAllOccurrences)
self.text = text
self._flags = flags
def next_result(self):
self._widget.search(self.text, self._flags)
def prev_result(self):
# The int() here serves as a QFlags constructor to create a copy of the
# QFlags instance rather as a reference. I don't know why it works this
# way, but it does.
flags = int(self._flags)
if flags & QWebPage.FindBackward:
flags &= ~QWebPage.FindBackward
else:
flags |= QWebPage.FindBackward
self._widget.search(self.text, flags)
class WebKitCaret(browsertab.AbstractCaret):
"""QtWebKit implementations related to moving the cursor/selection."""
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret:
return
settings = self._widget.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = bool(self.selection())
if self._widget.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self._widget.clearFocus()
self._widget.setFocus(Qt.OtherFocusReason)
# 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.
if not self.selection():
self._widget.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot()
def _on_mode_left(self):
settings = self._widget.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self._widget.hasSelection():
# Remove selection if it exists
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
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)
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)
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)
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)
def move_to_end_of_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if sys.platform == 'win32': # pragma: no cover
act.append(QWebPage.MoveToPreviousChar)
else:
act = [QWebPage.SelectNextWord]
if sys.platform == 'win32': # pragma: no cover
act.append(QWebPage.SelectPreviousChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_next_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if sys.platform != 'win32': # pragma: no branch
act.append(QWebPage.MoveToNextChar)
else:
act = [QWebPage.SelectNextWord]
if sys.platform != 'win32': # pragma: no branch
act.append(QWebPage.SelectNextChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
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)
def move_to_start_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfLine
else:
act = QWebPage.SelectStartOfLine
self._widget.triggerPageAction(act)
def move_to_end_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfLine
else:
act = QWebPage.SelectEndOfLine
self._widget.triggerPageAction(act)
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)
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)
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)
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)
def move_to_start_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfDocument
else:
act = QWebPage.SelectStartOfDocument
self._widget.triggerPageAction(act)
def move_to_end_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfDocument
else:
act = QWebPage.SelectEndOfDocument
self._widget.triggerPageAction(act)
def toggle_selection(self):
self.selection_enabled = not self.selection_enabled
mainwindow = objreg.get('main-window', scope='window',
window=self._win_id)
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
def has_selection(self):
return self._widget.hasSelection()
def selection(self, html=False):
if html:
return self._widget.selectedHtml()
return self._widget.selectedText()
def follow_selected(self, *, tab=False):
if not self.has_selection():
return
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
if tab:
self._widget.page().open_target = usertypes.ClickTarget.tab
self._tab.run_js_async(
'window.getSelection().anchorNode.parentNode.click()')
else:
selection = self.selection(html=True)
try:
selected_element = xml.etree.ElementTree.fromstring(
'<html>{}</html>'.format(selection)).find('a')
except xml.etree.ElementTree.ParseError:
raise browsertab.WebTabError('Could not parse selected '
'element!')
if selected_element is not None:
try:
url = selected_element.attrib['href']
except KeyError:
raise browsertab.WebTabError('Anchor element without '
'href!')
url = self._tab.url().resolved(QUrl(url))
if tab:
self._tab.new_tab_requested.emit(url)
else:
self._tab.openurl(url)
class WebKitZoom(browsertab.AbstractZoom):
"""QtWebKit implementations related to zooming."""
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
def factor(self):
return self._widget.zoomFactor()
class WebKitScroller(browsertab.AbstractScroller):
"""QtWebKit implementations related to scrolling."""
# FIXME:qtwebengine When to use the main frame, when the current one?
def pos_px(self):
return self._widget.page().mainFrame().scrollPosition()
def pos_perc(self):
return self._widget.scroll_pos
def to_point(self, point):
self._widget.page().mainFrame().setScrollPosition(point)
def delta(self, x=0, y=0):
qtutils.check_overflow(x, 'int')
qtutils.check_overflow(y, 'int')
self._widget.page().mainFrame().scroll(x, y)
def delta_page(self, x=0.0, y=0.0):
if y.is_integer():
y = int(y)
if y == 0:
pass
elif y < 0:
self.page_up(count=-y)
elif y > 0:
self.page_down(count=y)
y = 0
if x == 0 and y == 0:
return
size = self._widget.page().mainFrame().geometry()
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)]:
if val is not None:
val = qtutils.check_overflow(val, 'int', fatal=False)
frame = self._widget.page().mainFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
continue
frame.setScrollBarValue(orientation, int(m * val / 100))
def _key_press(self, key, count=1, getter_name=None, direction=None):
frame = self._widget.page().mainFrame()
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
getter = None if getter_name is None else getattr(frame, getter_name)
# FIXME:qtwebengine needed?
# self._widget.setFocus()
for _ in range(count):
# Abort scrolling if the minimum/maximum was reached.
if (getter is not None and
frame.scrollBarValue(direction) == getter(direction)):
return
self._widget.keyPressEvent(press_evt)
self._widget.keyReleaseEvent(release_evt)
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()
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
class WebKitHistory(browsertab.AbstractHistory):
"""QtWebKit implementations related to page history."""
def current_idx(self):
return self._history.currentItemIndex()
def back(self):
self._history.back()
def forward(self):
self._history.forward()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def serialize(self):
return qtutils.serialize(self._history)
def deserialize(self, data):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
stream, _data, user_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
for i, data in enumerate(user_data):
self._history.itemAt(i).setUserData(data)
cur_data = self._history.currentItem().userData()
if cur_data is not None:
if 'zoom' in cur_data:
self._tab.zoom.set_factor(cur_data['zoom'])
if ('scroll-pos' in cur_data and
self._tab.scroll.pos_px() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial(
self._tab.scroll.to_point, cur_data['scroll-pos']))
class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser."""
def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id)
widget = webview.WebView(win_id, self.tab_id, tab=self)
self.history = WebKitHistory(self)
self.scroll = WebKitScroller(parent=self)
self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = WebKitZoom(win_id=win_id, parent=self)
self.search = WebKitSearch(parent=self)
self._set_widget(widget)
self._connect_signals()
self.zoom.set_default()
self.backend = usertypes.Backend.QtWebKit
def openurl(self, url):
self._widget.openurl(url)
def url(self):
return self._widget.cur_url
def progress(self):
return self._widget.progress
def load_status(self):
return self._widget.load_status
def dump_async(self, callback, *, plain=False):
frame = self._widget.page().mainFrame()
if plain:
callback(frame.toPlainText())
else:
callback(frame.toHtml())
def run_js_async(self, code, callback=None):
result = self._widget.page().mainFrame().evaluateJavaScript(code)
if callback is not None:
callback(result)
def icon(self):
return self._widget.icon()
def shutdown(self):
self._widget.shutdown()
def reload(self, *, force=False):
if force:
action = QWebPage.ReloadAndBypassCache
else:
action = QWebPage.Reload
self._widget.triggerPageAction(action)
def stop(self):
self._widget.stop()
def title(self):
return self._widget.title()
def clear_ssl_errors(self):
nam = self._widget.page().networkAccessManager()
nam.clear_all_ssl_errors()
def set_html(self, html, base_url):
self._widget.setHtml(html, base_url)
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.load_progress)
frame.loadStarted.connect(self._on_load_started)
view.scroll_pos_changed.connect(self.scroll.perc_changed)
view.titleChanged.connect(self.title_changed)
view.url_text_changed.connect(self.url_text_changed)
view.load_status_changed.connect(self.load_status_changed)
view.shutting_down.connect(self.shutting_down)
# 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 https://github.com/The-Compiler/qutebrowser/issues/84
frame.loadFinished.connect(lambda:
self.load_finished.emit(
not self._widget.page().error_occurred))
# Emit iconChanged with a QIcon like QWebEngineView does.
view.iconChanged.connect(lambda:
self.icon_changed.emit(self._widget.icon()))

View File

@ -21,8 +21,7 @@
import functools
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
QTimer)
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
@ -31,7 +30,7 @@ from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.browser import pdfjs
from qutebrowser.browser.webkit import http, tabhistory
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
objreg, debug, urlutils)
@ -243,23 +242,6 @@ class BrowserPage(QWebPage):
else:
nam.shutdown()
def load_history(self, entries):
"""Load the history from a list of TabHistoryItem objects."""
stream, _data, user_data = tabhistory.serialize(entries)
history = self.history()
qtutils.deserialize_stream(stream, history)
for i, data in enumerate(user_data):
history.itemAt(i).setUserData(data)
cur_data = history.currentItem().userData()
if cur_data is not None:
frame = self.mainFrame()
if 'zoom' in cur_data:
frame.page().view().zoom_perc(cur_data['zoom'] * 100)
if ('scroll-pos' in cur_data and
frame.scrollPosition() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial(
frame.setScrollPosition, cur_data['scroll-pos']))
def display_content(self, reply, mimetype):
"""Display a QNetworkReply with an explicitly set mimetype."""
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
@ -436,7 +418,7 @@ class BrowserPage(QWebPage):
if data is None:
return
if 'zoom' in data:
frame.page().view().zoom_perc(data['zoom'] * 100)
frame.page().view().tab.zoom.set_factor(data['zoom'])
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos'])

View File

@ -20,10 +20,8 @@
"""The main browser widgets."""
import sys
import itertools
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings
@ -36,41 +34,23 @@ from qutebrowser.browser import hints
from qutebrowser.browser.webkit import webpage, webelem
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'success_https',
'error', 'warn', 'loading'])
tab_id_gen = itertools.count(0)
class WebView(QWebView):
"""One browser tab in TabbedBrowser.
Our own subclass of a QWebView with some added bells and whistles.
"""Custom QWebView subclass with qutebrowser-specific features.
Attributes:
tab: The WebKitTab object for this WebView
hintmanager: The HintManager instance for this view.
progress: loading progress of this page.
scroll_pos: The current scroll position as (x%, y%) tuple.
statusbar_message: The current javascript statusbar message.
inspector: The QWebInspector used for this webview.
load_status: loading status of this page (index into LoadStatus)
viewing_source: Whether the webview is currently displaying source
code.
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
registry: The ObjectRegistry associated with this tab.
tab_id: The tab ID of the view.
win_id: The window ID of the view.
search_text: The text of the last search.
search_flags: The search flags of the last search.
_tab_id: The tab ID of the view.
_has_ssl_errors: Whether SSL errors occurred during loading.
_zoom: A NeighborList with the zoom levels.
_old_scroll_pos: The old scroll position.
_check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode.
_default_zoom_changed: Whether the zoom was changed from the default.
_ignore_wheel_event: Ignore the next wheel event.
See https://github.com/The-Compiler/qutebrowser/issues/395
@ -81,6 +61,9 @@ class WebView(QWebView):
linkHovered: QWebPages linkHovered signal exposed.
load_status_changed: The loading status changed
url_text_changed: Current URL string changed.
mouse_wheel_zoom: Emitted when the page should be zoomed because the
mousewheel was used with ctrl.
arg 1: The angle delta of the wheel event (QPoint)
shutting_down: Emitted when the view is shutting down.
"""
@ -89,57 +72,39 @@ class WebView(QWebView):
load_status_changed = pyqtSignal(str)
url_text_changed = pyqtSignal(str)
shutting_down = pyqtSignal()
mouse_wheel_zoom = pyqtSignal(QPoint)
def __init__(self, win_id, parent=None):
def __init__(self, win_id, tab_id, tab, parent=None):
super().__init__(parent)
if sys.platform == 'darwin' and qtutils.version_check('5.4'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/The-Compiler/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion'))
self.tab = tab
self.win_id = win_id
self.load_status = LoadStatus.none
self.load_status = usertypes.LoadStatus.none
self._check_insertmode = False
self.inspector = None
self.scroll_pos = (-1, -1)
self.statusbar_message = ''
self._old_scroll_pos = (-1, -1)
self._zoom = None
self._has_ssl_errors = False
self._ignore_wheel_event = False
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.selection_enabled = False
self.init_neighborlist()
self._set_bg_color()
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
# For some reason, this signal doesn't get disconnected automatically
# when the WebView is destroyed on older PyQt versions.
# See https://github.com/The-Compiler/qutebrowser/issues/390
self.destroyed.connect(functools.partial(
cfg.changed.disconnect, self.init_neighborlist))
self.cur_url = QUrl()
self.progress = 0
self.registry = objreg.ObjectRegistry()
self.tab_id = next(tab_id_gen)
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('webview', self, registry=self.registry)
self._tab_id = tab_id
page = self._init_page()
hintmanager = hints.HintManager(win_id, self.tab_id, self)
hintmanager = hints.HintManager(win_id, self._tab_id, self)
hintmanager.mouse_event.connect(self.on_mouse_event)
hintmanager.start_hinting.connect(page.on_start_hinting)
hintmanager.stop_hinting.connect(page.on_stop_hinting)
objreg.register('hintmanager', hintmanager, registry=self.registry)
objreg.register('hintmanager', hintmanager, scope='tab', window=win_id,
tab=tab_id)
mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left)
self.viewing_source = False
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
self._default_zoom_changed = False
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.urlChanged.connect(self.on_url_changed)
@ -161,7 +126,7 @@ class WebView(QWebView):
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
self.setPage(page)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
@ -176,7 +141,7 @@ class WebView(QWebView):
def __repr__(self):
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
return utils.get_repr(self, tab_id=self.tab_id, url=url)
return utils.get_repr(self, tab_id=self._tab_id, url=url)
def __del__(self):
# Explicitly releasing the page here seems to prevent some segfaults
@ -193,7 +158,7 @@ class WebView(QWebView):
def _set_load_status(self, val):
"""Setter for load_status."""
if not isinstance(val, LoadStatus):
if not isinstance(val, usertypes.LoadStatus):
raise TypeError("Type {} is no LoadStatus member!".format(val))
log.webview.debug("load status for {}: {}".format(repr(self), val))
self.load_status = val
@ -210,14 +175,8 @@ class WebView(QWebView):
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Reinitialize the zoom neighborlist if related config changed."""
if section == 'ui' and option in ('zoom-levels', 'default-zoom'):
if not self._default_zoom_changed:
self.setZoomFactor(float(config.get('ui', 'default-zoom')) /
100)
self._default_zoom_changed = False
self.init_neighborlist()
elif section == 'input' and option == 'rocker-gestures':
"""Update rocker gestures/background color."""
if section == 'input' and option == 'rocker-gestures':
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
else:
@ -225,13 +184,6 @@ class WebView(QWebView):
elif section == 'colors' and option == 'webpage.bg':
self._set_bg_color()
def init_neighborlist(self):
"""Initialize the _zoom neighborlist."""
levels = config.get('ui', 'zoom-levels')
self._zoom = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
self._zoom.fuzzyval = config.get('ui', 'default-zoom')
def _mousepress_backforward(self, e):
"""Handle back/forward mouse button presses.
@ -381,33 +333,6 @@ class WebView(QWebView):
bridge = objreg.get('js-bridge')
frame.addToJavaScriptWindowObject('qute', bridge)
def zoom_perc(self, perc, fuzzyval=True):
"""Zoom to a given zoom percentage.
Args:
perc: The zoom percentage as int.
fuzzyval: Whether to set the NeighborLists fuzzyval.
"""
if fuzzyval:
self._zoom.fuzzyval = int(perc)
if perc < 0:
raise ValueError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
self._default_zoom_changed = True
def zoom(self, offset):
"""Increase/Decrease the zoom level.
Args:
offset: The offset in the zoom level list.
Return:
The new zoom percentage.
"""
level = self._zoom.getitem(offset)
self.zoom_perc(level, fuzzyval=False)
return level
@pyqtSlot('QUrl')
def on_url_changed(self, url):
"""Update cur_url when URL has changed.
@ -431,9 +356,8 @@ class WebView(QWebView):
def on_load_started(self):
"""Leave insert/hint mode and set vars when a new page is loading."""
self.progress = 0
self.viewing_source = False
self._has_ssl_errors = False
self._set_load_status(LoadStatus.loading)
self._set_load_status(usertypes.LoadStatus.loading)
@pyqtSlot()
def on_load_finished(self):
@ -446,14 +370,14 @@ class WebView(QWebView):
ok = not self.page().error_occurred
if ok and not self._has_ssl_errors:
if self.cur_url.scheme() == 'https':
self._set_load_status(LoadStatus.success_https)
self._set_load_status(usertypes.LoadStatus.success_https)
else:
self._set_load_status(LoadStatus.success)
self._set_load_status(usertypes.LoadStatus.success)
elif ok:
self._set_load_status(LoadStatus.warn)
self._set_load_status(usertypes.LoadStatus.warn)
else:
self._set_load_status(LoadStatus.error)
self._set_load_status(usertypes.LoadStatus.error)
if not self.title():
self.titleChanged.emit(self.url().toDisplayString())
self._handle_auto_insert_mode(ok)
@ -486,25 +410,6 @@ class WebView(QWebView):
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = bool(self.page().selectedText())
if self.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self.clearFocus()
self.setFocus(Qt.OtherFocusReason)
# 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.
if not self.page().selectedText():
self.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
@ -513,15 +418,6 @@ class WebView(QWebView):
usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self.hasSelection():
# Remove selection if it exists
self.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False
self.setFocusPolicy(Qt.WheelFocus)
def search(self, text, flags):
@ -590,7 +486,8 @@ class WebView(QWebView):
"support that!")
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self.win_id)
return tabbed_browser.tabopen(background=False)
# pylint: disable=protected-access
return tabbed_browser.tabopen(background=False)._widget
def paintEvent(self, e):
"""Extend paintEvent to emit a signal if the scroll position changed.
@ -672,14 +569,6 @@ class WebView(QWebView):
return
if e.modifiers() & Qt.ControlModifier:
e.accept()
divider = config.get('input', 'mouse-zoom-divider')
factor = self.zoomFactor() + e.angleDelta().y() / divider
if factor < 0:
return
perc = int(100 * factor)
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._zoom.fuzzyval = perc
self.setZoomFactor(factor)
self._default_zoom_changed = True
self.mouse_wheel_zoom.emit(e.angleDelta())
else:
super().wheelEvent(e)

View File

@ -80,6 +80,8 @@ class Command:
parser: The ArgumentParser to use to parse this command.
flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored.
backend: Which backend the command works with (or None if it works with
both)
_qute_args: The saved data from @cmdutils.argument
_needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in.
@ -92,7 +94,8 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global'):
no_cmd_split=False, star_args_optional=False, scope='global',
backend=None):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-locals
if modes is not None and not_modes is not None:
@ -123,6 +126,8 @@ class Command:
self.ignore_args = ignore_args
self.handler = handler
self.no_cmd_split = no_cmd_split
self.backend = backend
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
@ -170,10 +175,22 @@ class Command:
raise cmdexc.PrerequisitesError(
"{}: This command is not allowed in {} mode.".format(
self.name, mode_names))
if self._needs_js and not QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
raise cmdexc.PrerequisitesError(
"{}: This command needs javascript enabled.".format(self.name))
backend_mapping = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
used_backend = backend_mapping[objreg.get('args').backend]
if self.backend is not None and used_backend != self.backend:
raise cmdexc.PrerequisitesError(
"{}: Only available with {} "
"backend.".format(self.name, self.backend.name))
if self.deprecated:
message.warning(win_id, '{} is deprecated - {}'.format(
self.name, self.deprecated))
@ -483,6 +500,9 @@ class Command:
dbgout = ["command called:", self.name]
if args:
dbgout.append(str(args))
elif args is None:
args = []
if count is not None:
dbgout.append("(count={})".format(count))
log.commands.debug(' '.join(dbgout))
@ -497,8 +517,8 @@ class Command:
e.status, e))
return
self._count = count
posargs, kwargs = self._get_call_args(win_id)
self._check_prerequisites(win_id)
posargs, kwargs = self._get_call_args(win_id)
log.commands.debug('Calling {}'.format(
debug_utils.format_call(self.handler, posargs, kwargs)))
self.handler(*posargs, **kwargs)

View File

@ -26,7 +26,7 @@ import tempfile
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
from qutebrowser.utils import message, log, objreg, standarddir
from qutebrowser.commands import runners, cmdexc
from qutebrowser.commands import runners
from qutebrowser.config import config
from qutebrowser.misc import guiprocess
from qutebrowser.browser.webkit import downloads
@ -86,6 +86,10 @@ class _BaseUserscriptRunner(QObject):
_proc: The GUIProcess which is being executed.
_win_id: The window ID this runner is associated with.
_cleaned_up: Whether temporary files were cleaned up.
_text_stored: Set when the page text was stored async.
_html_stored: Set when the page html was stored async.
_args: Arguments to pass to _run_process.
_kwargs: Keyword arguments to pass to _run_process.
Signals:
got_cmd: Emitted when a new command arrived and should be executed.
@ -101,9 +105,41 @@ class _BaseUserscriptRunner(QObject):
self._win_id = win_id
self._filepath = None
self._proc = None
self._env = None
self._env = {}
self._text_stored = False
self._html_stored = False
self._args = None
self._kwargs = None
def _run_process(self, cmd, *args, env, verbose):
def store_text(self, text):
"""Called as callback when the text is ready from the web backend."""
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.txt',
delete=False) as txt_file:
txt_file.write(text)
self._env['QUTE_TEXT'] = txt_file.name
self._text_stored = True
log.procs.debug("Text stored from webview")
if self._text_stored and self._html_stored:
log.procs.debug("Both text/HTML stored, kicking off userscript!")
self._run_process(*self._args, **self._kwargs)
def store_html(self, html):
"""Called as callback when the html is ready from the web backend."""
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.html',
delete=False) as html_file:
html_file.write(html)
self._env['QUTE_HTML'] = html_file.name
self._html_stored = True
log.procs.debug("HTML stored from webview")
if self._text_stored and self._html_stored:
log.procs.debug("Both text/HTML stored, kicking off userscript!")
self._run_process(*self._args, **self._kwargs)
def _run_process(self, cmd, *args, env=None, verbose=False):
"""Start the given command.
Args:
@ -112,7 +148,7 @@ class _BaseUserscriptRunner(QObject):
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
"""
self._env = {'QUTE_FIFO': self._filepath}
self._env['QUTE_FIFO'] = self._filepath
if env is not None:
self._env.update(env)
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
@ -144,18 +180,19 @@ class _BaseUserscriptRunner(QObject):
fn, e))
self._filepath = None
self._proc = None
self._env = None
self._env = {}
self._text_stored = False
self._html_stored = False
def run(self, cmd, *args, env=None, verbose=False):
"""Run the userscript given.
def prepare_run(self, *args, **kwargs):
"""Prepare running the userscript given.
Needs to be overridden by subclasses.
The script will actually run after store_text and store_html have been
called.
Args:
cmd: The command to be started.
*args: The arguments to hand to the command
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
Passed to _run_process.
"""
raise NotImplementedError
@ -190,7 +227,10 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
super().__init__(win_id, parent)
self._reader = None
def run(self, cmd, *args, env=None, verbose=False):
def prepare_run(self, *args, **kwargs):
self._args = args
self._kwargs = kwargs
try:
# tempfile.mktemp is deprecated and discouraged, but we use it here
# to create a FIFO since the only other alternative would be to
@ -209,8 +249,6 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
self._reader = _QtFIFOReader(self._filepath)
self._reader.got_line.connect(self.got_cmd)
self._run_process(cmd, *args, env=env, verbose=verbose)
@pyqtSlot()
def on_proc_finished(self):
self._cleanup()
@ -280,86 +318,35 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
"""Read back the commands when the process finished."""
self._cleanup()
def run(self, cmd, *args, env=None, verbose=False):
def prepare_run(self, *args, **kwargs):
self._args = args
self._kwargs = kwargs
try:
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
except OSError as e:
message.error(self._win_id, "Error while creating tempfile: "
"{}".format(e))
return
self._run_process(cmd, *args, env=env, verbose=verbose)
class _DummyUserscriptRunner(QObject):
class UnsupportedError(Exception):
"""Simple dummy runner which displays an error when using userscripts.
"""Raised when userscripts aren't supported on this platform."""
Used on unknown systems since we don't know what (or if any) approach will
work there.
Signals:
finished: Always emitted.
"""
finished = pyqtSignal()
def __init__(self, win_id, parent=None):
# pylint: disable=unused-argument
super().__init__(parent)
def run(self, cmd, *args, env=None, verbose=False):
"""Print an error as userscripts are not supported."""
# pylint: disable=unused-argument,unused-variable
self.finished.emit()
raise cmdexc.CommandError(
"Userscripts are not supported on this platform!")
def __str__(self):
return "Userscripts are not supported on this platform!"
# Here we basically just assign a generic UserscriptRunner class which does the
# right thing depending on the platform.
if os.name == 'posix':
UserscriptRunner = _POSIXUserscriptRunner
elif os.name == 'nt': # pragma: no cover
UserscriptRunner = _WindowsUserscriptRunner
else: # pragma: no cover
UserscriptRunner = _DummyUserscriptRunner
def run_async(tab, cmd, *args, win_id, env, verbose=False):
"""Run a userscript after dumping page html/source.
def store_source(frame):
"""Store HTML/plaintext in files.
This writes files containing the HTML/plaintext source of the page, and
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
Args:
frame: The QWebFrame to get the info from, or None to do nothing.
Return:
A dictionary with the needed environment variables.
Warning:
The caller is responsible to delete the files after using them!
"""
if frame is None:
return {}
env = {}
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.html',
delete=False) as html_file:
html_file.write(frame.toHtml())
env['QUTE_HTML'] = html_file.name
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.txt',
delete=False) as txt_file:
txt_file.write(frame.toPlainText())
env['QUTE_TEXT'] = txt_file.name
return env
def run(cmd, *args, win_id, env, verbose=False):
"""Convenience method to run a userscript.
Raises:
UnsupportedError if userscripts are not supported on the current
platform.
Args:
tab: The WebKitTab/WebEngineTab to get the source from.
cmd: The userscript binary to run.
*args: The arguments to pass to the userscript.
win_id: The window id the userscript is executed in.
@ -369,7 +356,14 @@ def run(cmd, *args, win_id, env, verbose=False):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
commandrunner = runners.CommandRunner(win_id, parent=tabbed_browser)
runner = UserscriptRunner(win_id, tabbed_browser)
if os.name == 'posix':
runner = _POSIXUserscriptRunner(win_id, tabbed_browser)
elif os.name == 'nt': # pragma: no cover
runner = _WindowsUserscriptRunner(win_id, tabbed_browser)
else: # pragma: no cover
raise UnsupportedError
runner.got_cmd.connect(
lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd)))
@ -398,6 +392,9 @@ def run(cmd, *args, win_id, env, verbose=False):
"userscripts", cmd)
log.misc.debug("Userscript to run: {}".format(cmd_path))
runner.run(cmd_path, *args, env=env, verbose=verbose)
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)
runner.prepare_run(cmd_path, *args, env=env, verbose=verbose)
tab.dump_async(runner.store_html)
tab.dump_async(runner.store_text, plain=True)

View File

@ -22,7 +22,7 @@
from collections import defaultdict
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from qutebrowser.browser.webkit import webview
from qutebrowser.browser import browsertab
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils, utils
from qutebrowser.commands import cmdutils
@ -193,7 +193,7 @@ class TabCompletionModel(base.BaseCompletionModel):
"""Add hooks to new windows."""
window.tabbed_browser.new_tab.connect(self.on_new_tab)
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_new_tab(self, tab):
"""Add hooks to new tabs."""
tab.url_text_changed.connect(self.rebuild)

View File

@ -345,7 +345,6 @@ class MainWindow(QWidget):
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
tabs.cur_load_started.connect(status.txt.on_load_started)

View File

@ -329,12 +329,12 @@ class StatusBar(QWidget):
log.statusbar.debug("Setting command_active to {}".format(val))
self._command_active = val
elif mode == usertypes.KeyMode.caret:
webview = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
tab = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
log.statusbar.debug("Setting caret_mode - val {}, selection "
"{}".format(val, webview.selection_enabled))
"{}".format(val, tab.caret.selection_enabled))
if val:
if webview.selection_enabled:
if tab.caret.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._caret_mode = CaretMode.selection
else:

View File

@ -21,8 +21,8 @@
from PyQt5.QtCore import pyqtSlot
from qutebrowser.browser import browsertab
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.browser.webkit import webview
class Percentage(textbase.TextBase):
@ -46,10 +46,12 @@ class Percentage(textbase.TextBase):
self.setText('[top]')
elif y == 100:
self.setText('[bot]')
elif y is None:
self.setText('[???]')
else:
self.setText('[{:2}%]'.format(y))
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Update scroll position when tab changed."""
self.set_perc(*tab.scroll_pos)
self.set_perc(*tab.scroll.pos_perc())

View File

@ -22,9 +22,9 @@
from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
from qutebrowser.browser.webkit import webview
from qutebrowser.browser import browsertab
from qutebrowser.config import style
from qutebrowser.utils import utils
from qutebrowser.utils import utils, usertypes
class Progress(QProgressBar):
@ -59,15 +59,15 @@ class Progress(QProgressBar):
self.setValue(0)
self.show()
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Set the correct value when the current tab changed."""
if self is None: # pragma: no branch
# This should never happen, but for some weird reason it does
# sometimes.
return # pragma: no cover
self.setValue(tab.progress)
if tab.load_status == webview.LoadStatus.loading:
self.setValue(tab.progress())
if tab.load_status() == usertypes.LoadStatus.loading:
self.show()
else:
self.hide()

View File

@ -21,10 +21,10 @@
from PyQt5.QtCore import pyqtSlot
from qutebrowser.browser import browsertab
from qutebrowser.config import config
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.utils import usertypes, log, objreg
from qutebrowser.browser.webkit import webview
class Text(textbase.TextBase):
@ -99,7 +99,7 @@ class Text(textbase.TextBase):
"""Clear jstext when page loading started."""
self._jstext = ''
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Set the correct jstext when the current tab changed."""
self._jstext = tab.statusbar_message

View File

@ -21,7 +21,7 @@
from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
from qutebrowser.browser.webkit import webview
from qutebrowser.browser import browsertab
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.config import style
from qutebrowser.utils import usertypes
@ -119,11 +119,11 @@ class UrlText(textbase.TextBase):
Args:
status_str: The LoadStatus as string.
"""
status = webview.LoadStatus[status_str]
if status in (webview.LoadStatus.success,
webview.LoadStatus.success_https,
webview.LoadStatus.error,
webview.LoadStatus.warn):
status = usertypes.LoadStatus[status_str]
if status in (usertypes.LoadStatus.success,
usertypes.LoadStatus.success_https,
usertypes.LoadStatus.error,
usertypes.LoadStatus.warn):
self._normal_url_type = UrlType[status_str]
else:
self._normal_url_type = UrlType.normal
@ -140,8 +140,8 @@ class UrlText(textbase.TextBase):
self._normal_url_type = UrlType.normal
self._update_url()
@pyqtSlot(str, str, str)
def set_hover_url(self, link, _title, _text):
@pyqtSlot(str)
def set_hover_url(self, link):
"""Setter to be used as a Qt slot.
Saves old shown URL in self._old_url and restores it later if a link is
@ -149,8 +149,6 @@ class UrlText(textbase.TextBase):
Args:
link: The link which was hovered (string)
_title: The title of the hovered link (string)
_text: The text of the hovered link (string)
"""
if link:
qurl = QUrl(link)
@ -162,10 +160,10 @@ class UrlText(textbase.TextBase):
self._hover_url = None
self._update_url()
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Update URL if the tab changed."""
self._hover_url = None
self._normal_url = tab.cur_url.toDisplayString()
self.on_load_status_changed(tab.load_status.name)
self._normal_url = tab.url().toDisplayString()
self.on_load_status_changed(tab.load_status().name)
self._update_url()

View File

@ -29,8 +29,7 @@ from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget
from qutebrowser.browser import signalfilter
from qutebrowser.browser.webkit import webview
from qutebrowser.browser import signalfilter, browsertab
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message)
@ -56,8 +55,8 @@ class TabbedBrowser(tabwidget.TabWidget):
emitted if the signal occurred in the current tab.
Attributes:
search_text/search_flags: Search parameters which are shared between
all tabs.
search_text/search_options: Search parameters which are shared between
all tabs.
_win_id: The window ID this tabbedbrowser is associated with.
_filter: A SignalFilter instance.
_now_focused: The tab which is focused now.
@ -71,13 +70,13 @@ class TabbedBrowser(tabwidget.TabWidget):
default_window_icon: The qutebrowser window icon
Signals:
cur_progress: Progress of the current tab changed (loadProgress).
cur_load_started: Current tab started loading (loadStarted)
cur_load_finished: Current tab finished loading (loadFinished)
cur_progress: Progress of the current tab changed (load_progress).
cur_load_started: Current tab started loading (load_started)
cur_load_finished: Current tab finished loading (load_finished)
cur_statusbar_message: Current tab got a statusbar message
(statusBarMessage)
cur_url_text_changed: Current URL text changed.
cur_link_hovered: Link hovered in current tab (linkHovered)
cur_link_hovered: Link hovered in current tab (link_hovered)
cur_scroll_perc_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
@ -86,7 +85,7 @@ class TabbedBrowser(tabwidget.TabWidget):
resized: Emitted when the browser window has resized, so the completion
widget can adjust its size to it.
arg: The new size.
current_tab_changed: The current tab changed to the emitted WebView.
current_tab_changed: The current tab changed to the emitted tab.
new_tab: Emits the new WebView and its index when a new tab is opened.
"""
@ -95,13 +94,13 @@ class TabbedBrowser(tabwidget.TabWidget):
cur_load_finished = pyqtSignal(bool)
cur_statusbar_message = pyqtSignal(str)
cur_url_text_changed = pyqtSignal(str)
cur_link_hovered = pyqtSignal(str, str, str)
cur_link_hovered = pyqtSignal(str)
cur_scroll_perc_changed = pyqtSignal(int, int)
cur_load_status_changed = pyqtSignal(str)
close_window = pyqtSignal()
resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(webview.WebView)
new_tab = pyqtSignal(webview.WebView, int)
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
new_tab = pyqtSignal(browsertab.AbstractTab, int)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
@ -117,7 +116,7 @@ class TabbedBrowser(tabwidget.TabWidget):
self._filter = signalfilter.SignalFilter(win_id, self)
self._now_focused = None
self.search_text = None
self.search_flags = 0
self.search_options = {}
self._local_marks = {}
self._global_marks = {}
self.default_window_icon = self.window().windowIcon()
@ -170,22 +169,21 @@ class TabbedBrowser(tabwidget.TabWidget):
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
page = tab.page()
frame = page.mainFrame()
# filtered signals
tab.linkHovered.connect(
tab.link_hovered.connect(
self._filter.create(self.cur_link_hovered, tab))
tab.loadProgress.connect(
tab.load_progress.connect(
self._filter.create(self.cur_progress, tab))
frame.loadFinished.connect(
tab.load_finished.connect(
self._filter.create(self.cur_load_finished, tab))
frame.loadStarted.connect(
tab.load_started.connect(
self._filter.create(self.cur_load_started, tab))
tab.statusBarMessage.connect(
self._filter.create(self.cur_statusbar_message, tab))
tab.scroll_pos_changed.connect(
# https://github.com/The-Compiler/qutebrowser/issues/1579
# tab.statusBarMessage.connect(
# self._filter.create(self.cur_statusbar_message, tab))
tab.scroll.perc_changed.connect(
self._filter.create(self.cur_scroll_perc_changed, tab))
tab.scroll_pos_changed.connect(self.on_scroll_pos_changed)
tab.scroll.perc_changed.connect(self.on_scroll_pos_changed)
tab.url_text_changed.connect(
self._filter.create(self.cur_url_text_changed, tab))
tab.load_status_changed.connect(
@ -193,18 +191,19 @@ class TabbedBrowser(tabwidget.TabWidget):
tab.url_text_changed.connect(
functools.partial(self.on_url_text_changed, tab))
# misc
tab.titleChanged.connect(
tab.title_changed.connect(
functools.partial(self.on_title_changed, tab))
tab.iconChanged.connect(
tab.icon_changed.connect(
functools.partial(self.on_icon_changed, tab))
tab.loadProgress.connect(
tab.load_progress.connect(
functools.partial(self.on_load_progress, tab))
frame.loadFinished.connect(
tab.load_finished.connect(
functools.partial(self.on_load_finished, tab))
frame.loadStarted.connect(
tab.load_started.connect(
functools.partial(self.on_load_started, tab))
page.windowCloseRequested.connect(
tab.window_close_requested.connect(
functools.partial(self.on_window_close_requested, tab))
tab.new_tab_requested.connect(self.tabopen)
def current_url(self):
"""Get the URL of the current tab.
@ -265,11 +264,11 @@ class TabbedBrowser(tabwidget.TabWidget):
window=self._win_id):
objreg.delete('last-focused-tab', scope='window',
window=self._win_id)
if tab.cur_url.isValid():
history_data = qtutils.serialize(tab.history())
entry = UndoEntry(tab.cur_url, history_data)
if tab.url().isValid():
history_data = tab.history.serialize()
entry = UndoEntry(tab.url(), history_data)
self._undo_stack.append(entry)
elif tab.cur_url.isEmpty():
elif tab.url().isEmpty():
# There are some good reasons why a URL could be empty
# (target="_blank" with a download, see [1]), so we silently ignore
# this.
@ -279,7 +278,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# We display a warnings for URLs which are not empty but invalid -
# but we don't return here because we want the tab to close either
# way.
urlutils.invalid_url_error(self._win_id, tab.cur_url, "saving tab")
urlutils.invalid_url_error(self._win_id, tab.url(), "saving tab")
tab.shutdown()
self.removeTab(idx)
tab.deleteLater()
@ -291,13 +290,13 @@ class TabbedBrowser(tabwidget.TabWidget):
use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.count() == 1
no_history = self.widget(0).history().count() == 1
no_history = len(self.widget(0).history) == 1
urls = {
'blank': QUrl('about:blank'),
'startpage': QUrl(config.get('general', 'startpage')[0]),
'default-page': config.get('general', 'default-page'),
}
first_tab_url = self.widget(0).page().mainFrame().requestedUrl()
first_tab_url = self.widget(0).url()
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
@ -312,7 +311,7 @@ class TabbedBrowser(tabwidget.TabWidget):
else:
newtab = self.tabopen(url, background=False)
qtutils.deserialize(history_data, newtab.history())
newtab.history.deserialize(history_data)
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
@ -338,7 +337,7 @@ class TabbedBrowser(tabwidget.TabWidget):
return
self.close_tab(tab)
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_window_close_requested(self, widget):
"""Close a tab with a widget given."""
try:
@ -347,6 +346,7 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Requested to close {!r} which does not "
"exist!".format(widget))
@pyqtSlot('QUrl')
@pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None, explicit=False):
"""Open a new tab with a given URL.
@ -378,10 +378,13 @@ class TabbedBrowser(tabwidget.TabWidget):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id)
return tabbed_browser.tabopen(url, background, explicit)
tab = webview.WebView(self._win_id, self)
tab = browsertab.create(win_id=self._win_id, parent=self)
self._connect_tab_signals(tab)
idx = self._get_new_tab_idx(explicit)
self.insertTab(idx, tab, "")
if url is not None:
tab.openurl(url)
if background is None:
@ -457,8 +460,8 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted...
return
self.update_tab_title(idx)
if tab.keep_icon:
tab.keep_icon = False
if tab.data.keep_icon:
tab.data.keep_icon = False
else:
self.setTabIcon(idx, QIcon())
if (config.get('tabs', 'tabs-are-windows') and
@ -475,11 +478,11 @@ class TabbedBrowser(tabwidget.TabWidget):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'load started')
@pyqtSlot(webview.WebView, str)
@pyqtSlot(browsertab.AbstractTab, str)
def on_title_changed(self, tab, text):
"""Set the title of a tab.
Slot for the titleChanged signal of any tab.
Slot for the title_changed signal of any tab.
Args:
tab: The WebView where the title was changed.
@ -499,7 +502,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if idx == self.currentIndex():
self.update_window_title()
@pyqtSlot(webview.WebView, str)
@pyqtSlot(browsertab.AbstractTab, str)
def on_url_text_changed(self, tab, url):
"""Set the new URL as title if there's no title yet.
@ -515,14 +518,15 @@ class TabbedBrowser(tabwidget.TabWidget):
if not self.page_title(idx):
self.set_page_title(idx, url)
@pyqtSlot(webview.WebView)
def on_icon_changed(self, tab):
@pyqtSlot(browsertab.AbstractTab, QIcon)
def on_icon_changed(self, tab, icon):
"""Set the icon of a tab.
Slot for the iconChanged signal of any tab.
Args:
tab: The WebView where the title was changed.
icon: The new icon
"""
if not config.get('tabs', 'show-favicons'):
return
@ -531,9 +535,9 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.setTabIcon(idx, tab.icon())
self.setTabIcon(idx, icon)
if config.get('tabs', 'tabs-are-windows'):
self.window().setWindowIcon(tab.icon())
self.window().setWindowIcon(icon)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
@ -589,25 +593,20 @@ class TabbedBrowser(tabwidget.TabWidget):
if idx == self.currentIndex():
self.update_window_title()
def on_load_finished(self, tab):
"""Adjust tab indicator when loading finished.
We don't take loadFinished's ok argument here as it always seems to be
true when the QWebPage has an ErrorPageExtension implemented.
See https://github.com/The-Compiler/qutebrowser/issues/84
"""
def on_load_finished(self, tab, ok):
"""Adjust tab indicator when loading finished."""
try:
idx = self._tab_index(tab)
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
if tab.page().error_occurred:
color = config.get('colors', 'tabs.indicator.error')
else:
if ok:
start = config.get('colors', 'tabs.indicator.start')
stop = config.get('colors', 'tabs.indicator.stop')
system = config.get('colors', 'tabs.indicator.system')
color = utils.interpolate_color(start, stop, 100, system)
else:
color = config.get('colors', 'tabs.indicator.error')
self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx)
if idx == self.currentIndex():
@ -653,7 +652,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if key != "'":
message.error(self._win_id, "Failed to set mark: url invalid")
return
point = self.currentWidget().page().currentFrame().scrollPosition()
point = self.currentWidget().scroll.pos_px()
if key.isupper():
self._global_marks[key] = point, url
@ -674,7 +673,7 @@ class TabbedBrowser(tabwidget.TabWidget):
except qtutils.QtValueError:
urlkey = None
frame = self.currentWidget().page().currentFrame()
tab = self.currentWidget()
if key.isupper():
if key in self._global_marks:
@ -684,7 +683,7 @@ class TabbedBrowser(tabwidget.TabWidget):
def callback(ok):
if ok:
self.cur_load_finished.disconnect(callback)
frame.setScrollPosition(point)
tab.scroll.to_point(point)
self.openurl(url, newtab=False)
self.cur_load_finished.connect(callback)
@ -700,6 +699,6 @@ class TabbedBrowser(tabwidget.TabWidget):
# "'" would just jump to the current position every time
self.set_mark("'")
frame.setScrollPosition(point)
tab.scroll.to_point(point)
else:
message.error(self._win_id, "Mark {} is not set".format(key))

View File

@ -29,7 +29,6 @@ from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils, usertypes
from qutebrowser.config import config
from qutebrowser.browser.webkit import webview
PixelMetrics = usertypes.enum('PixelMetrics', ['icon_padding'],
@ -108,17 +107,17 @@ class TabWidget(QTabWidget):
def get_tab_fields(self, idx):
"""Get the tab field data."""
widget = self.widget(idx)
tab = self.widget(idx)
page_title = self.page_title(idx)
fields = {}
fields['id'] = widget.tab_id
fields['id'] = tab.tab_id
fields['title'] = page_title
fields['title_sep'] = ' - ' if page_title else ''
fields['perc_raw'] = widget.progress
fields['perc_raw'] = tab.progress()
if widget.load_status == webview.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(widget.progress)
if tab.load_status() == usertypes.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(tab.progress())
else:
fields['perc'] = ''
@ -127,8 +126,10 @@ class TabWidget(QTabWidget):
except qtutils.QtValueError:
fields['host'] = ''
y = widget.scroll_pos[1]
if y <= 0:
y = tab.scroll.pos_perc()[1]
if y is None:
scroll_pos = '???'
elif y <= 0:
scroll_pos = 'top'
elif y >= 100:
scroll_pos = 'bot'
@ -224,11 +225,11 @@ class TabWidget(QTabWidget):
Return:
The tab URL as QUrl.
"""
widget = self.widget(idx)
if widget is None:
tab = self.widget(idx)
if tab is None:
url = QUrl()
else:
url = widget.cur_url
url = tab.url()
# It's possible for url to be invalid, but the caller will handle that.
qtutils.ensure_valid(url)
return url

View File

@ -477,6 +477,29 @@ class ExceptionCrashDialog(_CrashDialog):
else:
self.reject()
@pyqtSlot()
def on_report_clicked(self):
"""Ignore reports with the QtWebEngine backend.
FIXME:qtwebengine Remove this when QtWebEngine is working better!
"""
try:
backend = objreg.get('args').backend
except Exception:
backend = 'webkit'
if backend == 'webkit':
super().on_report_clicked()
return
title = "Crash reports disabled with QtWebEngine!"
text = ("You're using the QtWebEngine backend which is not intended "
"for general usage yet. Crash reports with that backend have "
"been disabled.")
box = msgbox.msgbox(parent=self, title=title, text=text,
icon=QMessageBox.Critical)
box.finished.connect(self.finish)
class FatalCrashDialog(_CrashDialog):

View File

@ -116,7 +116,7 @@ class CrashHandler(QObject):
window=win_id)
for tab in tabbed_browser.widgets():
try:
urlstr = tab.cur_url.toString(
urlstr = tab.url().toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
win_pages.append(urlstr)

View File

@ -264,6 +264,17 @@ def check_libraries():
_die(text, e)
def maybe_import_webengine():
"""Import QtWebEngineWidgets before QApplication is created.
See https://github.com/The-Compiler/qutebrowser/pull/1629#issuecomment-231613099
"""
try:
from PyQt5 import QtWebEngineWidgets
except ImportError:
pass
def remove_inputhook():
"""Remove the PyQt input hook.
@ -309,4 +320,5 @@ def earlyinit(args):
check_ssl_support()
remove_inputhook()
check_libraries()
maybe_import_webengine()
init_log(args)

View File

@ -130,6 +130,55 @@ class SessionManager(QObject):
else:
return True
def _save_tab_item(self, tab, idx, item):
"""Save a single history item in a tab.
Args:
tab: The tab to save.
idx: The index of the current history item.
item: The history item.
Return:
A dict with the saved data for this item.
"""
data = {
'url': bytes(item.url().toEncoded()).decode('ascii'),
}
if item.title():
data['title'] = item.title()
else:
# https://github.com/The-Compiler/qutebrowser/issues/879
if tab.history.current_idx() == idx:
data['title'] = tab.title()
else:
data['title'] = data['url']
if item.originalUrl() != item.url():
encoded = item.originalUrl().toEncoded()
data['original-url'] = bytes(encoded).decode('ascii')
if tab.history.current_idx() == idx:
data['active'] = True
try:
user_data = item.userData()
except AttributeError:
# QtWebEngine
user_data = None
if tab.history.current_idx() == idx:
pos = tab.scroll.pos_px()
data['zoom'] = tab.zoom.factor()
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
elif user_data is not None:
if 'zoom' in user_data:
data['zoom'] = user_data['zoom']
if 'scroll-pos' in user_data:
pos = user_data['scroll-pos']
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
return data
def _save_tab(self, tab, active):
"""Get a dict with data for a single tab.
@ -140,42 +189,9 @@ class SessionManager(QObject):
data = {'history': []}
if active:
data['active'] = True
history = tab.page().history()
for idx, item in enumerate(history.items()):
for idx, item in enumerate(tab.history):
qtutils.ensure_valid(item)
item_data = {
'url': bytes(item.url().toEncoded()).decode('ascii'),
}
if item.title():
item_data['title'] = item.title()
else:
# https://github.com/The-Compiler/qutebrowser/issues/879
if history.currentItemIndex() == idx:
item_data['title'] = tab.page().mainFrame().title()
else:
item_data['title'] = item_data['url']
if item.originalUrl() != item.url():
encoded = item.originalUrl().toEncoded()
item_data['original-url'] = bytes(encoded).decode('ascii')
if history.currentItemIndex() == idx:
item_data['active'] = True
user_data = item.userData()
if history.currentItemIndex() == idx:
pos = tab.page().mainFrame().scrollPosition()
item_data['zoom'] = tab.zoomFactor()
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
elif user_data is not None:
if 'zoom' in user_data:
item_data['zoom'] = user_data['zoom']
if 'scroll-pos' in user_data:
pos = user_data['scroll-pos']
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
item_data = self._save_tab_item(tab, idx, item)
data['history'].append(item_data)
return data
@ -300,9 +316,9 @@ class SessionManager(QObject):
active=active, user_data=user_data)
entries.append(entry)
if active:
new_tab.titleChanged.emit(histentry['title'])
new_tab.title_changed.emit(histentry['title'])
try:
new_tab.page().load_history(entries)
new_tab.history.load_items(entries)
except ValueError as e:
raise SessionError(e)

View File

@ -69,6 +69,10 @@ def get_argparser():
'tab-silent', 'tab-bg-silent', 'window'],
help="How URLs should be opened if there is already a "
"qutebrowser instance running.")
parser.add_argument('--backend', choices=['webkit', 'webengine'],
# help="Which backend to use.",
help=argparse.SUPPRESS, default='webkit')
parser.add_argument('--json-args', help=argparse.SUPPRESS)
parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS)

View File

@ -29,6 +29,7 @@ import faulthandler
import traceback
import warnings
import json
import inspect
from PyQt5 import QtCore
# Optional imports
@ -130,6 +131,15 @@ sessions = logging.getLogger('sessions')
ram_handler = None
def stub(suffix=''):
"""Show a STUB: message for the calling function."""
function = inspect.stack()[1][3]
text = "STUB: {}".format(function)
if suffix:
text = '{} ({})'.format(text, suffix)
misc.warning(text)
class CriticalQtWarning(Exception):
"""Exception raised when there's a critical Qt warning."""

View File

@ -153,6 +153,8 @@ def _get_tab_registry(win_id, tab_id):
win_id = window.win_id
elif win_id is not None:
window = window_registry[win_id]
else:
raise TypeError("window is None with scope tab!")
if tab_id == 'current':
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)

View File

@ -246,6 +246,15 @@ Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
'err_config', 'err_key_config'], is_int=True, start=0)
# Load status of a tab
LoadStatus = enum('LoadStatus', ['none', 'success', 'success_https', 'error',
'warn', 'loading'])
# Backend of a tab
Backend = enum('Backend', ['QtWebKit', 'QtWebEngine'])
class Question(QObject):
"""A question asked to the user, e.g. via the status bar.

View File

@ -70,6 +70,8 @@ PERFECT_FILES = [
('tests/unit/browser/test_signalfilter.py',
'qutebrowser/browser/signalfilter.py'),
# ('tests/unit/browser/test_tab.py',
# 'qutebrowser/browser/tab.py'),
('tests/unit/keyinput/test_basekeyparser.py',
'qutebrowser/keyinput/basekeyparser.py'),

View File

@ -760,7 +760,7 @@ Feature: Tab management
And I open data/search.html in a new tab
And I open data/scroll.html in a new tab
And I run :buffer "Searching text"
And I wait for "Current tab changed, focusing <qutebrowser.browser.webkit.webview.WebView tab_id=* url='http://localhost:*/data/search.html'>" in the log
And I wait for "Current tab changed, focusing <qutebrowser.browser.* tab_id=* url='http://localhost:*/data/search.html'>" in the log
Then the following tabs should be open:
- data/title.html
- data/search.html (active)
@ -777,7 +777,7 @@ Feature: Tab management
And I open data/caret.html in a new window
And I open data/paste_primary.html in a new tab
And I run :buffer "Scrolling"
And I wait for "Focus object changed: <qutebrowser.browser.webkit.webview.WebView tab_id=* url='http://localhost:*/data/scroll.html'>" in the log
And I wait for "Focus object changed: <qutebrowser.browser.* tab_id=* url='http://localhost:*/data/scroll.html'>" in the log
Then the session should look like:
windows:
- active: true
@ -816,7 +816,7 @@ Feature: Tab management
And I open data/paste_primary.html in a new tab
And I wait until data/caret.html is loaded
And I run :buffer "0/2"
And I wait for "Focus object changed: <qutebrowser.browser.webkit.webview.WebView tab_id=* url='http://localhost:*/data/search.html'>" in the log
And I wait for "Focus object changed: <qutebrowser.browser.* tab_id=* url='http://localhost:*/data/search.html'>" in the log
Then the session should look like:
windows:
- active: true

View File

@ -202,21 +202,22 @@ class QuteProc(testprocess.Process):
self._log(log_line)
start_okay_message_load = (
"load status for <qutebrowser.browser.webkit.webview.WebView "
"tab_id=0 url='about:blank'>: LoadStatus.success")
"load status for <qutebrowser.browser.* tab_id=0 "
"url='about:blank'>: LoadStatus.success")
start_okay_message_focus = (
"Focus object changed: "
"<qutebrowser.browser.webkit.webview.WebView "
"tab_id=0 url='about:blank'>")
"<qutebrowser.browser.* tab_id=0 url='about:blank'>")
if (log_line.category == 'ipc' and
log_line.message.startswith("Listening as ")):
self._ipc_socket = log_line.message.split(' ', maxsplit=2)[2]
elif (log_line.category == 'webview' and
log_line.message == start_okay_message_load):
testutils.pattern_match(pattern=start_okay_message_load,
value=log_line.message)):
self._is_ready('load')
elif (log_line.category == 'misc' and
log_line.message == start_okay_message_focus):
testutils.pattern_match(pattern=start_okay_message_focus,
value=log_line.message)):
self._is_ready('focus')
elif (log_line.category == 'init' and
log_line.module == 'standarddir' and
@ -291,8 +292,7 @@ class QuteProc(testprocess.Process):
# Try to complain about the most common mistake when accidentally
# loading external resources.
is_ddg_load = testutils.pattern_match(
pattern="load status for <qutebrowser.browser.webview.WebView "
"tab_id=* url='*duckduckgo*'>: *",
pattern="load status for <* tab_id=* url='*duckduckgo*'>: *",
value=msg.message)
return msg.loglevel > logging.INFO or is_js_error or is_ddg_load
@ -442,8 +442,7 @@ class QuteProc(testprocess.Process):
assert url
pattern = re.compile(
r"(load status for "
r"<qutebrowser\.browser\.webkit\.webview\.WebView "
r"(load status for <qutebrowser\.browser\..* "
r"tab_id=\d+ url='{url}/?'>: LoadStatus\.{load_status}|fetch: "
r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format(
load_status=re.escape(load_status), url=re.escape(url)))

View File

@ -29,6 +29,7 @@ import collections
import itertools
import textwrap
import unittest.mock
import types
import pytest
@ -37,8 +38,9 @@ from qutebrowser.config import config
from qutebrowser.utils import objreg
from qutebrowser.browser.webkit import cookies
from qutebrowser.misc import savemanager
from qutebrowser.keyinput import modeman
from PyQt5.QtCore import QEvent, QSize, Qt
from PyQt5.QtCore import PYQT_VERSION, QEvent, QSize, Qt
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtNetwork import QNetworkCookieJar
@ -122,6 +124,14 @@ def tab_registry(win_registry):
objreg.delete('tab-registry', scope='window', window=0)
@pytest.fixture
def fake_web_tab(stubs, tab_registry, qapp):
"""Fixture providing the FakeWebTab *class*."""
if PYQT_VERSION < 0x050600:
pytest.skip('Causes segfaults, see #1638')
return stubs.FakeWebTab
def _generate_cmdline_tests():
"""Generate testcases for test_split_binding."""
# pylint: disable=invalid-name
@ -377,3 +387,20 @@ def fake_save_manager():
objreg.register('save-manager', fake_save_manager)
yield fake_save_manager
objreg.delete('save-manager')
@pytest.yield_fixture
def fake_args():
ns = types.SimpleNamespace()
objreg.register('args', ns)
yield ns
objreg.delete('args')
@pytest.yield_fixture
def mode_manager(win_registry, config_stub, qapp):
config_stub.data = {'input': {'forward-unbound-keys': 'auto'}}
mm = modeman.ModeManager(0)
objreg.register('mode-manager', mm, scope='window', window=0)
yield mm
objreg.delete('mode-manager', scope='window', window=0)

View File

@ -53,7 +53,8 @@ class MessageMock:
self._caplog = caplog
self.messages = []
def _handle(self, level, win_id, text, immediately=False):
def _handle(self, level, win_id, text, immediately=False, *,
stack=None): # pylint: disable=unused-variable
log_levels = {
Level.error: logging.ERROR,
Level.info: logging.INFO,

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=invalid-name
# pylint: disable=invalid-name,abstract-method
"""Fake objects/stubs."""
@ -27,10 +27,12 @@ from unittest import mock
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
QNetworkCacheMetaData)
from PyQt5.QtWidgets import QCommonStyle, QWidget, QLineEdit
from PyQt5.QtWidgets import QCommonStyle, QLineEdit
from qutebrowser.browser.webkit import webview, history
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import history
from qutebrowser.config import configexc
from qutebrowser.utils import usertypes
from qutebrowser.mainwindow import mainwindow
@ -223,24 +225,44 @@ def fake_qprocess():
return m
class FakeWebView(QWidget):
class FakeWebTabScroller(browsertab.AbstractScroller):
"""Fake WebView which can be added to a tab."""
"""Fake AbstractScroller to use in tests."""
url_text_changed = pyqtSignal(str)
shutting_down = pyqtSignal()
def __init__(self, url=FakeUrl(), title='', tab_id=0):
def __init__(self, pos_perc):
super().__init__()
self.progress = 0
self.scroll_pos = (-1, -1)
self.load_status = webview.LoadStatus.none
self.tab_id = tab_id
self.cur_url = url
self.title = title
self._pos_perc = pos_perc
def pos_perc(self):
return self._pos_perc
class FakeWebTab(browsertab.AbstractTab):
"""Fake AbstractTab to use in tests."""
def __init__(self, url=FakeUrl(), title='', tab_id=0, *,
scroll_pos_perc=(0, 0),
load_status=usertypes.LoadStatus.success,
progress=0):
super().__init__(win_id=0)
self._load_status = load_status
self._title = title
self._url = url
self._progress = progress
self.scroll = FakeWebTabScroller(scroll_pos_perc)
def url(self):
return self.cur_url
return self._url
def title(self):
return self._title
def progress(self):
return self._progress
def load_status(self):
return self._load_status
class FakeSignal:
@ -522,7 +544,7 @@ class TabbedBrowserStub(QObject):
"""Stub for the tabbed-browser object."""
new_tab = pyqtSignal(webview.WebView, int)
new_tab = pyqtSignal(browsertab.AbstractTab, int)
def __init__(self, parent=None):
super().__init__(parent)
@ -536,7 +558,7 @@ class TabbedBrowserStub(QObject):
return self.tabs[i]
def page_title(self, i):
return self.tabs[i].title
return self.tabs[i].title()
def on_tab_close_requested(self, idx):
del self.tabs[idx]

View File

@ -69,13 +69,10 @@ class BaseDirStub:
self.basedir = None
@pytest.yield_fixture
def basedir():
@pytest.fixture
def basedir(fake_args):
"""Register a Fake basedir."""
args = BaseDirStub()
objreg.register('args', args)
yield
objreg.delete('args')
fake_args.basedir = None
class FakeDownloadItem(QObject):

View File

@ -0,0 +1,101 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
import pytest
from PyQt5.QtCore import PYQT_VERSION, pyqtSignal, QPoint
from qutebrowser.browser import browsertab
from qutebrowser.keyinput import modeman
try:
from PyQt5.QtWebKitWidgets import QWebView
class WebView(QWebView):
mouse_wheel_zoom = pyqtSignal(QPoint)
except ImportError:
WebView = None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
class WebEngineView(QWebEngineView):
mouse_wheel_zoom = pyqtSignal(QPoint)
except ImportError:
WebEngineView = None
@pytest.mark.skipif(PYQT_VERSION < 0x050600,
reason='Causes segfaults, see #1638')
@pytest.mark.parametrize('view', [WebView, WebEngineView])
def test_tab(qtbot, view, config_stub, tab_registry):
config_stub.data = {
'input': {
'forward-unbound-keys': 'auto'
},
'ui': {
'zoom-levels': [100],
'default-zoom': 100,
}
}
if view is None:
pytest.skip("View not available")
w = view()
qtbot.add_widget(w)
tab_w = browsertab.AbstractTab(win_id=0)
qtbot.add_widget(tab_w)
tab_w.show()
assert tab_w.win_id == 0
assert tab_w._widget is None
mode_manager = modeman.ModeManager(0)
tab_w.history = browsertab.AbstractHistory(tab_w)
tab_w.scroll = browsertab.AbstractScroller(parent=tab_w)
tab_w.caret = browsertab.AbstractCaret(win_id=tab_w.win_id,
mode_manager=mode_manager,
tab=tab_w, parent=tab_w)
tab_w.zoom = browsertab.AbstractZoom(win_id=tab_w.win_id)
tab_w.search = browsertab.AbstractSearch(parent=tab_w)
tab_w._set_widget(w)
assert tab_w._widget is w
assert tab_w.history._tab is tab_w
assert tab_w.history._history is w.history()
assert w.parent() is tab_w
class TestTabData:
def test_known_attr(self):
data = browsertab.TabData()
assert not data.keep_icon
data.keep_icon = True
assert data.keep_icon
def test_unknown_attr(self):
data = browsertab.TabData()
with pytest.raises(AttributeError):
data.bar = 42 # pylint: disable=assigning-non-slot
with pytest.raises(AttributeError):
data.bar # pylint: disable=pointless-statement

View File

@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=unused-variable
"""Tests for qutebrowser.commands.cmdutils."""
import pytest
@ -32,6 +34,19 @@ def clear_globals(monkeypatch):
monkeypatch.setattr(cmdutils, 'aliases', [])
def _get_cmd(*args, **kwargs):
"""Get a command object created via @cmdutils.register.
Args:
Passed to @cmdutils.register decorator
"""
@cmdutils.register(*args, **kwargs)
def fun():
"""Blah."""
pass
return cmdutils.cmd_dict['fun']
class TestCheckOverflow:
def test_good(self):
@ -87,8 +102,6 @@ class TestCheckExclusive:
class TestRegister:
# pylint: disable=unused-variable
def test_simple(self):
@cmdutils.register()
def fun():
@ -306,8 +319,6 @@ class TestArgument:
"""Test the @cmdutils.argument decorator."""
# pylint: disable=unused-variable
def test_invalid_argument(self):
with pytest.raises(ValueError) as excinfo:
@cmdutils.argument('foo')
@ -350,3 +361,51 @@ class TestArgument:
pass
assert str(excinfo.value) == "Argument marked as both count/win_id!"
class TestRun:
@pytest.fixture(autouse=True)
def patching(self, mode_manager, fake_args):
fake_args.backend = 'webkit'
@pytest.mark.parametrize('backend, used, ok', [
(usertypes.Backend.QtWebEngine, 'webengine', True),
(usertypes.Backend.QtWebEngine, 'webkit', False),
(usertypes.Backend.QtWebKit, 'webengine', False),
(usertypes.Backend.QtWebKit, 'webkit', True),
(None, 'webengine', True),
(None, 'webkit', True),
])
def test_backend(self, fake_args, backend, used, ok):
fake_args.backend = used
cmd = _get_cmd(backend=backend)
if ok:
cmd.run(win_id=0)
else:
with pytest.raises(cmdexc.PrerequisitesError) as excinfo:
cmd.run(win_id=0)
assert str(excinfo.value).endswith(' backend.')
def test_no_args(self):
cmd = _get_cmd()
cmd.run(win_id=0)
def test_instance_unavailable_with_backend(self, fake_args):
"""Test what happens when a backend doesn't have an objreg object.
For example, QtWebEngine doesn't have 'hintmanager' registered. We make
sure the backend checking happens before resolving the instance, so we
display an error instead of crashing.
"""
@cmdutils.register(instance='doesnotexist',
backend=usertypes.Backend.QtWebEngine)
def fun(self):
"""Blah."""
pass
fake_args.backend = 'webkit'
cmd = cmdutils.cmd_dict['fun']
with pytest.raises(cmdexc.PrerequisitesError) as excinfo:
cmd.run(win_id=0)
assert str(excinfo.value).endswith(' backend.')

View File

@ -26,7 +26,7 @@ import signal
import pytest
from PyQt5.QtCore import QFileSystemWatcher
from qutebrowser.commands import userscripts, cmdexc
from qutebrowser.commands import userscripts
@pytest.fixture(autouse=True)
@ -80,7 +80,9 @@ def test_command(qtbot, py_proc, runner):
f.write('foo\n')
""")
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.run(cmd, *args)
runner.prepare_run(cmd, *args)
runner.store_html('')
runner.store_text('')
assert blocker.args == ['foo']
@ -100,7 +102,9 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
""")
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.run(cmd, *args, env=env)
runner.prepare_run(cmd, *args, env=env)
runner.store_html('')
runner.store_text('')
data = blocker.args[0]
ret_env = json.loads(data)
@ -108,20 +112,16 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
assert 'QUTEBROWSER_TEST_2' in ret_env
def test_temporary_files(qtbot, tmpdir, py_proc, runner):
"""Make sure temporary files are passed and cleaned up correctly."""
text_file = tmpdir / 'text'
text_file.write('This is text')
html_file = tmpdir / 'html'
html_file.write('This is HTML')
env = {'QUTE_TEXT': str(text_file), 'QUTE_HTML': str(html_file)}
def test_source(qtbot, py_proc, runner):
"""Make sure the page source is read and cleaned up correctly."""
cmd, args = py_proc(r"""
import os
import json
data = {'html': None, 'text': None}
data = {
'html_file': os.environ['QUTE_HTML'],
'text_file': os.environ['QUTE_TEXT'],
}
with open(os.environ['QUTE_HTML'], 'r') as f:
data['html'] = f.read()
@ -136,76 +136,85 @@ def test_temporary_files(qtbot, tmpdir, py_proc, runner):
with qtbot.waitSignal(runner.finished, timeout=10000):
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.run(cmd, *args, env=env)
runner.prepare_run(cmd, *args)
runner.store_html('This is HTML')
runner.store_text('This is text')
data = blocker.args[0]
parsed = json.loads(data)
assert parsed['text'] == 'This is text'
assert parsed['html'] == 'This is HTML'
assert not text_file.exists()
assert not html_file.exists()
assert not os.path.exists(parsed['text_file'])
assert not os.path.exists(parsed['html_file'])
def test_command_with_error(qtbot, tmpdir, py_proc, runner):
text_file = tmpdir / 'text'
text_file.write('This is text')
env = {'QUTE_TEXT': str(text_file)}
def test_command_with_error(qtbot, py_proc, runner):
cmd, args = py_proc(r"""
import sys
import sys, os, json
with open(os.environ['QUTE_FIFO'], 'w') as f:
json.dump(os.environ['QUTE_TEXT'], f)
f.write('\n')
sys.exit(1)
""")
with qtbot.waitSignal(runner.finished, timeout=10000):
runner.run(cmd, *args, env=env)
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.prepare_run(cmd, *args)
runner.store_text('Hello World')
runner.store_html('')
assert not text_file.exists()
data = json.loads(blocker.args[0])
assert not os.path.exists(data)
def test_killed_command(qtbot, tmpdir, py_proc, runner):
text_file = tmpdir / 'text'
text_file.write('This is text')
pidfile = tmpdir / 'pid'
data_file = tmpdir / 'data'
watcher = QFileSystemWatcher()
watcher.addPath(str(tmpdir))
env = {'QUTE_TEXT': str(text_file)}
cmd, args = py_proc(r"""
import os
import time
import sys
import json
data = {
'pid': os.getpid(),
'text_file': os.environ['QUTE_TEXT'],
}
# We can't use QUTE_FIFO to transmit the PID because that wouldn't work
# on Windows, where QUTE_FIFO is only monitored after the script has
# exited.
with open(sys.argv[1], 'w') as f:
f.write(str(os.getpid()))
json.dump(data, f)
time.sleep(30)
""")
args.append(str(pidfile))
args.append(str(data_file))
with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
runner.run(cmd, *args, env=env)
runner.prepare_run(cmd, *args)
runner.store_text('Hello World')
runner.store_html('')
# Make sure the PID was written to the file, not just the file created
time.sleep(0.5)
data = json.load(data_file)
with qtbot.waitSignal(runner.finished):
os.kill(int(pidfile.read()), signal.SIGTERM)
os.kill(int(data['pid']), signal.SIGTERM)
assert not text_file.exists()
assert not os.path.exists(data['text_file'])
def test_temporary_files_failed_cleanup(caplog, qtbot, tmpdir, py_proc,
runner):
def test_temporary_files_failed_cleanup(caplog, qtbot, py_proc, runner):
"""Delete a temporary file from the script so cleanup fails."""
test_file = tmpdir / 'test'
test_file.write('foo')
cmd, args = py_proc(r"""
import os
os.remove(os.environ['QUTE_HTML'])
@ -213,41 +222,18 @@ def test_temporary_files_failed_cleanup(caplog, qtbot, tmpdir, py_proc,
with caplog.at_level(logging.ERROR):
with qtbot.waitSignal(runner.finished, timeout=10000):
runner.run(cmd, *args, env={'QUTE_HTML': str(test_file)})
runner.prepare_run(cmd, *args)
runner.store_text('')
runner.store_html('')
assert len(caplog.records) == 1
expected = "Failed to delete tempfile {} (".format(test_file)
expected = "Failed to delete tempfile"
assert caplog.records[0].message.startswith(expected)
def test_dummy_runner(qtbot):
runner = userscripts._DummyUserscriptRunner(0)
with pytest.raises(cmdexc.CommandError):
with qtbot.waitSignal(runner.finished):
runner.run('cmd', 'arg')
def test_store_source_none():
assert userscripts.store_source(None) == {}
def test_store_source(stubs):
expected_text = 'This is text'
expected_html = 'This is HTML'
frame = stubs.FakeWebFrame(plaintext=expected_text, html=expected_html)
env = userscripts.store_source(frame)
with open(env['QUTE_TEXT'], 'r', encoding='utf-8') as f:
text = f.read()
with open(env['QUTE_HTML'], 'r', encoding='utf-8') as f:
html = f.read()
os.remove(env['QUTE_TEXT'])
os.remove(env['QUTE_HTML'])
assert set(env.keys()) == {'QUTE_TEXT', 'QUTE_HTML'}
assert text == expected_text
assert html == expected_html
assert env['QUTE_TEXT'].endswith('.txt')
assert env['QUTE_HTML'].endswith('.html')
def test_unsupported(monkeypatch, tabbed_browser_stubs):
monkeypatch.setattr(userscripts.os, 'name', 'toaster')
with pytest.raises(userscripts.UnsupportedError) as excinfo:
userscripts.run_async(tab=None, cmd=None, win_id=0, env=None)
expected = "Userscripts are not supported on this platform!"
assert str(excinfo.value) == expected

View File

@ -304,15 +304,15 @@ def test_session_completion(session_manager_stub):
]
def test_tab_completion(stubs, qtbot, app_stub, win_registry,
def test_tab_completion(fake_web_tab, app_stub, win_registry,
tabbed_browser_stubs):
tabbed_browser_stubs[0].tabs = [
stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0),
stubs.FakeWebView(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
stubs.FakeWebView(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2),
]
tabbed_browser_stubs[1].tabs = [
stubs.FakeWebView(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
]
actual = _get_completions(miscmodels.TabCompletionModel())
assert actual == [
@ -327,16 +327,16 @@ def test_tab_completion(stubs, qtbot, app_stub, win_registry,
]
def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry,
def test_tab_completion_delete(fake_web_tab, qtbot, app_stub, win_registry,
tabbed_browser_stubs):
"""Verify closing a tab by deleting it from the completion widget."""
tabbed_browser_stubs[0].tabs = [
stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0),
stubs.FakeWebView(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
stubs.FakeWebView(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
]
tabbed_browser_stubs[1].tabs = [
stubs.FakeWebView(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
]
model = miscmodels.TabCompletionModel()
view = _mock_view_index(model, 0, 1, qtbot)

View File

@ -21,8 +21,6 @@
import os
import os.path
import configparser
import types
import argparse
import collections
import shutil
from unittest import mock
@ -339,14 +337,18 @@ class TestConfigInit:
"""Test initializing of the config."""
@pytest.yield_fixture(autouse=True)
def patch(self):
def patch(self, fake_args):
objreg.register('app', QObject())
objreg.register('save-manager', mock.MagicMock())
args = argparse.Namespace(relaxed_config=False)
objreg.register('args', args)
fake_args.relaxed_config = False
old_standarddir_args = standarddir._args
yield
objreg.global_registry.clear()
objreg.delete('app')
objreg.delete('save-manager')
# registered by config.init()
objreg.delete('config')
objreg.delete('key-config')
objreg.delete('state-config')
standarddir._args = old_standarddir_args
@pytest.fixture
@ -361,12 +363,14 @@ class TestConfigInit:
}
return env
def test_config_none(self, monkeypatch, env):
def test_config_none(self, monkeypatch, env, fake_args):
"""Test initializing with config path set to None."""
args = types.SimpleNamespace(confdir='', datadir='', cachedir='',
basedir=None)
fake_args.confdir = ''
fake_args.datadir = ''
fake_args.cachedir = ''
fake_args.basedir = None
for k, v in env.items():
monkeypatch.setenv(k, v)
standarddir.init(args)
standarddir.init(fake_args)
config.init()
assert not os.listdir(env['XDG_CONFIG_HOME'])

View File

@ -19,7 +19,6 @@
import pytest
from qutebrowser.keyinput import modeman as modeman_module
from qutebrowser.utils import usertypes
from PyQt5.QtCore import Qt, QObject, pyqtSignal
@ -40,11 +39,9 @@ class FakeKeyparser(QObject):
@pytest.fixture
def modeman(config_stub, qapp):
config_stub.data = {'input': {'forward-unbound-keys': 'auto'}}
mm = modeman_module.ModeManager(0)
mm.register(usertypes.KeyMode.normal, FakeKeyparser())
return mm
def modeman(mode_manager):
mode_manager.register(usertypes.KeyMode.normal, FakeKeyparser())
return mode_manager
@pytest.mark.parametrize('key, modifiers, text, filtered', [

View File

@ -20,16 +20,11 @@
"""Test Percentage widget."""
import collections
import pytest
from qutebrowser.mainwindow.statusbar.percentage import Percentage
FakeTab = collections.namedtuple('FakeTab', 'scroll_pos')
@pytest.fixture
def percentage(qtbot):
"""Fixture providing a Percentage widget."""
@ -44,6 +39,7 @@ def percentage(qtbot):
(75, '[75%]'),
(25, '[25%]'),
(5, '[ 5%]'),
(None, '[???]'),
])
def test_percentage_text(percentage, y, expected):
"""Test text displayed by the widget based on the y position of a page.
@ -57,9 +53,9 @@ def test_percentage_text(percentage, y, expected):
assert percentage.text() == expected
def test_tab_change(percentage):
def test_tab_change(percentage, fake_web_tab):
"""Make sure the percentage gets changed correctly when switching tabs."""
percentage.set_perc(x=None, y=10)
tab = FakeTab([0, 20])
tab = fake_web_tab(scroll_pos_perc=(0, 20))
percentage.on_tab_changed(tab)
assert percentage.text() == '[20%]'

View File

@ -20,12 +20,10 @@
"""Test Progress widget."""
from collections import namedtuple
import pytest
from qutebrowser.browser.webkit import webview
from qutebrowser.mainwindow.statusbar.progress import Progress
from qutebrowser.utils import usertypes
@pytest.fixture
@ -55,28 +53,24 @@ def test_load_started(progress_widget):
assert progress_widget.isVisible()
# mock tab object
Tab = namedtuple('Tab', 'progress load_status')
@pytest.mark.parametrize('tab, expected_visible', [
(Tab(15, webview.LoadStatus.loading), True),
(Tab(100, webview.LoadStatus.success), False),
(Tab(100, webview.LoadStatus.error), False),
(Tab(100, webview.LoadStatus.warn), False),
(Tab(100, webview.LoadStatus.none), False),
@pytest.mark.parametrize('progress, load_status, expected_visible', [
(15, usertypes.LoadStatus.loading, True),
(100, usertypes.LoadStatus.success, False),
(100, usertypes.LoadStatus.error, False),
(100, usertypes.LoadStatus.warn, False),
(100, usertypes.LoadStatus.none, False),
])
def test_tab_changed(progress_widget, tab, expected_visible):
def test_tab_changed(fake_web_tab, progress_widget, progress, load_status,
expected_visible):
"""Test that progress widget value and visibility state match expectations.
This uses a dummy Tab object.
Args:
progress_widget: Progress widget that will be tested.
"""
tab = fake_web_tab(progress=progress, load_status=load_status)
progress_widget.on_tab_changed(tab)
actual = progress_widget.value(), progress_widget.isVisible()
expected = tab.progress, expected_visible
expected = tab.progress(), expected_visible
assert actual == expected

View File

@ -21,18 +21,11 @@
"""Test Statusbar url."""
import pytest
import collections
from qutebrowser.browser.webkit import webview
from qutebrowser.utils import usertypes
from qutebrowser.mainwindow.statusbar import url
@pytest.fixture
def tab_widget():
"""Fixture providing a fake tab widget."""
tab = collections.namedtuple('Tab', 'cur_url load_status')
tab.cur_url = collections.namedtuple('cur_url', 'toDisplayString')
return tab
from PyQt5.QtCore import QUrl
@pytest.fixture
@ -73,14 +66,14 @@ def test_set_url(url_widget, url_text):
assert url_widget.text() == ""
@pytest.mark.parametrize('url_text, title, text', [
('http://abc123.com/this/awesome/url.html', 'Awesome site', 'click me!'),
('https://supersecret.gov/nsa/files.txt', 'Secret area', None),
(None, None, 'did I break?!')
@pytest.mark.parametrize('url_text', [
'http://abc123.com/this/awesome/url.html',
'https://supersecret.gov/nsa/files.txt',
None,
])
def test_set_hover_url(url_widget, url_text, title, text):
def test_set_hover_url(url_widget, url_text):
"""Test text when hovering over a link."""
url_widget.set_hover_url(url_text, title, text)
url_widget.set_hover_url(url_text)
if url_text is not None:
assert url_widget.text() == url_text
assert url_widget._urltype == url.UrlType.hover
@ -99,18 +92,18 @@ def test_set_hover_url(url_widget, url_text, title, text):
])
def test_set_hover_url_encoded(url_widget, url_text, expected):
"""Test text when hovering over a percent encoded link."""
url_widget.set_hover_url(url_text, 'title', 'text')
url_widget.set_hover_url(url_text)
assert url_widget.text() == expected
assert url_widget._urltype == url.UrlType.hover
@pytest.mark.parametrize('status, expected', [
(webview.LoadStatus.success, url.UrlType.success),
(webview.LoadStatus.success_https, url.UrlType.success_https),
(webview.LoadStatus.error, url.UrlType.error),
(webview.LoadStatus.warn, url.UrlType.warn),
(webview.LoadStatus.loading, url.UrlType.normal),
(webview.LoadStatus.none, url.UrlType.normal)
(usertypes.LoadStatus.success, url.UrlType.success),
(usertypes.LoadStatus.success_https, url.UrlType.success_https),
(usertypes.LoadStatus.error, url.UrlType.error),
(usertypes.LoadStatus.warn, url.UrlType.warn),
(usertypes.LoadStatus.loading, url.UrlType.normal),
(usertypes.LoadStatus.none, url.UrlType.normal)
])
def test_on_load_status_changed(url_widget, status, expected):
"""Test text when status is changed."""
@ -119,42 +112,36 @@ def test_on_load_status_changed(url_widget, status, expected):
assert url_widget._urltype == expected
@pytest.mark.parametrize('load_status, url_text', [
(url.UrlType.success, 'http://abc123.com/this/awesome/url.html'),
(url.UrlType.success, 'http://reddit.com/r/linux'),
(url.UrlType.success_https, 'www.google.com'),
(url.UrlType.success_https, 'https://supersecret.gov/nsa/files.txt'),
(url.UrlType.warn, 'www.shadysite.org/some/path/to/file/with/issues.htm'),
(url.UrlType.error, 'Th1$ i$ n0t @ n0rm@L uRL! P@n1c! <-->'),
(url.UrlType.error, None)
@pytest.mark.parametrize('load_status, qurl', [
(url.UrlType.success, QUrl('http://abc123.com/this/awesome/url.html')),
(url.UrlType.success, QUrl('http://reddit.com/r/linux')),
(url.UrlType.success_https, QUrl('www.google.com')),
(url.UrlType.success_https, QUrl('https://supersecret.gov/nsa/files.txt')),
(url.UrlType.warn, QUrl('www.shadysite.org/some/file/with/issues.htm')),
(url.UrlType.error, QUrl('invalid::/url')),
])
def test_on_tab_changed(url_widget, tab_widget, load_status, url_text):
tab_widget.load_status = load_status
tab_widget.cur_url.toDisplayString = lambda: url_text
def test_on_tab_changed(url_widget, fake_web_tab, load_status, qurl):
tab_widget = fake_web_tab(load_status=load_status, url=qurl)
url_widget.on_tab_changed(tab_widget)
if url_text is not None:
assert url_widget._urltype == load_status
assert url_widget.text() == url_text
else:
assert url_widget._urltype == url.UrlType.normal
assert url_widget.text() == ''
assert url_widget._urltype == load_status
assert url_widget.text() == qurl.toDisplayString()
@pytest.mark.parametrize('url_text, load_status, expected_status', [
('http://abc123.com/this/awesome/url.html', webview.LoadStatus.success,
('http://abc123.com/this/awesome/url.html', usertypes.LoadStatus.success,
url.UrlType.success),
('https://supersecret.gov/nsa/files.txt', webview.LoadStatus.success_https,
('https://supersecret.gov/nsa/files.txt', usertypes.LoadStatus.success_https,
url.UrlType.success_https),
('Th1$ i$ n0t @ n0rm@L uRL! P@n1c! <-->', webview.LoadStatus.error,
('Th1$ i$ n0t @ n0rm@L uRL! P@n1c! <-->', usertypes.LoadStatus.error,
url.UrlType.error),
('http://www.qutebrowser.org/CONTRIBUTING.html', webview.LoadStatus.loading,
('http://www.qutebrowser.org/CONTRIBUTING.html', usertypes.LoadStatus.loading,
url.UrlType.normal),
('www.whatisthisurl.com', webview.LoadStatus.warn, url.UrlType.warn)
('www.whatisthisurl.com', usertypes.LoadStatus.warn, url.UrlType.warn)
])
def test_normal_url(url_widget, url_text, load_status, expected_status):
url_widget.set_url(url_text)
url_widget.on_load_status_changed(load_status.name)
url_widget.set_hover_url(url_text, "", "")
url_widget.set_hover_url("", "", "")
url_widget.set_hover_url(url_text)
url_widget.set_hover_url("")
assert url_widget.text() == url_text
assert url_widget._urltype == expected_status

View File

@ -61,7 +61,7 @@ class TestTabWidget:
qtbot.addWidget(w)
return w
def test_small_icon_doesnt_crash(self, widget, qtbot, stubs):
def test_small_icon_doesnt_crash(self, widget, qtbot, fake_web_tab):
"""Test that setting a small icon doesn't produce a crash.
Regression test for #1015.
@ -69,7 +69,7 @@ class TestTabWidget:
# Size taken from issue report
pixmap = QPixmap(72, 1)
icon = QIcon(pixmap)
page = stubs.FakeWebView()
widget.addTab(page, icon, 'foobar')
tab = fake_web_tab()
widget.addTab(tab, icon, 'foobar')
widget.show()
qtbot.waitForWindowShown(widget)

View File

@ -42,9 +42,6 @@ from qutebrowser.utils import objreg, qtutils
from helpers import stubs
Args = collections.namedtuple('Args', 'basedir')
pytestmark = pytest.mark.usefixtures('qapp')

View File

@ -38,6 +38,10 @@ from qutebrowser.commands import cmdexc
pytestmark = pytest.mark.qt_log_ignore('QIODevice::read.*: device not open',
extend=True)
webengine_refactoring_xfail = pytest.mark.xfail(
True, reason='Broke during QtWebEngine refactoring, will be fixed after '
'sessions are refactored too.')
@pytest.fixture
def sess_man():
@ -166,6 +170,7 @@ class HistTester:
return ret[0]
@webengine_refactoring_xfail
class TestSaveTab:
@pytest.fixture
@ -350,6 +355,7 @@ class TestSaveAll:
data = sess_man._save_all()
assert not data['windows']
@webengine_refactoring_xfail
def test_normal(self, fake_windows, sess_man):
"""Test with some windows and tabs set up."""
data = sess_man._save_all()
@ -372,6 +378,7 @@ class TestSaveAll:
expected = {'windows': [win1, win2]}
assert data == expected
@webengine_refactoring_xfail
def test_no_active_window(self, sess_man, fake_windows, stubs,
monkeypatch):
qapp = stubs.FakeQApplication(active_window=None)
@ -491,6 +498,7 @@ class TestSave:
sess_man.save(str(session_path), load_next_time=True)
assert state_config['general']['session'] == str(session_path)
@webengine_refactoring_xfail
def test_utf_8_valid(self, tmpdir, sess_man, fake_history):
"""Make sure data containing valid UTF8 gets saved correctly."""
session_path = tmpdir / 'foo.yml'
@ -502,6 +510,7 @@ class TestSave:
data = session_path.read_text('utf-8')
assert 'title: foo☃bar' in data
@webengine_refactoring_xfail
def test_utf_8_invalid(self, tmpdir, sess_man, fake_history):
"""Make sure data containing invalid UTF8 raises SessionError."""
session_path = tmpdir / 'foo.yml'
@ -528,6 +537,7 @@ class TestSave:
@pytest.mark.skipif(
os.name == 'nt', reason="Test segfaults on Windows, see "
"https://github.com/The-Compiler/qutebrowser/issues/895")
@webengine_refactoring_xfail
def test_long_output(self, fake_windows, tmpdir, sess_man):
session_path = tmpdir / 'foo.yml'
@ -628,6 +638,7 @@ def fake_webview():
return FakeWebView()
@webengine_refactoring_xfail
class TestLoadTab:
def test_no_history(self, sess_man, fake_webview):
@ -728,6 +739,7 @@ class TestListSessions:
class TestSessionSave:
@webengine_refactoring_xfail
def test_normal_save(self, sess_man, tmpdir, fake_windows):
sess_file = tmpdir / 'foo.yml'
sess_man.session_save(0, str(sess_file), quiet=True)
@ -743,6 +755,7 @@ class TestSessionSave:
assert str(excinfo.value) == expected_text
assert not (tmpdir / '_foo.yml').exists()
@webengine_refactoring_xfail
def test_internal_with_force(self, tmpdir, fake_windows):
sess_man = sessions.SessionManager(str(tmpdir))
sess_man.session_save(0, '_foo', force=True, quiet=True)
@ -756,6 +769,7 @@ class TestSessionSave:
assert str(excinfo.value) == "No session loaded currently!"
@webengine_refactoring_xfail
def test_current_set(self, tmpdir, fake_windows):
sess_man = sessions.SessionManager(str(tmpdir))
sess_man._current = 'foo'
@ -768,6 +782,7 @@ class TestSessionSave:
assert str(excinfo.value).startswith('Error while saving session: ')
@webengine_refactoring_xfail
def test_message(self, sess_man, tmpdir, message_mock, fake_windows):
message_mock.patch('qutebrowser.misc.sessions.message')
sess_path = str(tmpdir / 'foo.yml')
@ -775,6 +790,7 @@ class TestSessionSave:
expected_text = 'Saved session {}.'.format(sess_path)
assert message_mock.getmsg(immediate=True).text == expected_text
@webengine_refactoring_xfail
def test_message_quiet(self, sess_man, tmpdir, message_mock, fake_windows):
message_mock.patch('qutebrowser.misc.sessions.message')
sess_path = str(tmpdir / 'foo.yml')

View File

@ -19,7 +19,6 @@
"""Tests for qutebrowser.utils.error."""
import sys
import collections
import logging
import pytest
@ -31,9 +30,6 @@ from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtWidgets import QMessageBox
Args = collections.namedtuple('Args', 'no_err_windows')
class Error(Exception):
pass
@ -47,14 +43,15 @@ class Error(Exception):
(ipc.Error, 'misc.ipc.Error', 'none'),
(Error, 'test_error.Error', 'none'),
])
def test_no_err_windows(caplog, exc, name, exc_text):
def test_no_err_windows(caplog, exc, name, exc_text, fake_args):
"""Test handle_fatal_exc with no_err_windows = True."""
fake_args.no_err_windows = True
try:
raise exc
except Exception as e:
with caplog.at_level(logging.ERROR):
error.handle_fatal_exc(e, Args(no_err_windows=True), 'title',
pre_text='pre', post_text='post')
error.handle_fatal_exc(e, fake_args, 'title', pre_text='pre',
post_text='post')
assert len(caplog.records) == 1
@ -82,7 +79,7 @@ def test_no_err_windows(caplog, exc, name, exc_text):
('foo', 'bar', 'foo: exception\n\nbar'),
('', 'bar', 'exception\n\nbar'),
], ids=repr)
def test_err_windows(qtbot, qapp, pre_text, post_text, expected):
def test_err_windows(qtbot, qapp, fake_args, pre_text, post_text, expected):
@pyqtSlot()
def err_window_check():
@ -97,6 +94,7 @@ def test_err_windows(qtbot, qapp, pre_text, post_text, expected):
finally:
w.close()
fake_args.no_err_windows = False
QTimer.singleShot(0, err_window_check)
error.handle_fatal_exc(ValueError("exception"), Args(no_err_windows=False),
'title', pre_text=pre_text, post_text=post_text)
error.handle_fatal_exc(ValueError("exception"), fake_args, 'title',
pre_text=pre_text, post_text=post_text)

View File

@ -267,3 +267,14 @@ class TestHideQtWarning:
with caplog.at_level(logging.WARNING, 'qt-tests'):
logger.warning(" Hello World ")
assert not caplog.records
@pytest.mark.parametrize('suffix, expected', [
('', 'STUB: test_stub'),
('foo', 'STUB: test_stub (foo)'),
])
def test_stub(caplog, suffix, expected):
with caplog.at_level(logging.WARNING, 'misc'):
log.stub(suffix)
assert len(caplog.records) == 1
assert caplog.records[0].message == expected