Merge branch 'qtwebengine'
This commit is contained in:
commit
8567fffdad
@ -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}$
|
||||
|
544
qutebrowser/browser/browsertab.py
Normal file
544
qutebrowser/browser/browsertab.py
Normal 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
@ -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:
|
||||
|
20
qutebrowser/browser/webengine/__init__.py
Normal file
20
qutebrowser/browser/webengine/__init__.py
Normal 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."""
|
332
qutebrowser/browser/webengine/webenginetab.py
Normal file
332
qutebrowser/browser/webengine/webenginetab.py
Normal 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)
|
45
qutebrowser/browser/webengine/webview.py
Normal file
45
qutebrowser/browser/webengine/webview.py
Normal 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)
|
@ -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)
|
||||
|
@ -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
|
||||
|
543
qutebrowser/browser/webkit/webkittab.py
Normal file
543
qutebrowser/browser/webkit/webkittab.py
Normal 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()))
|
@ -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'])
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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):
|
||||
|
101
tests/unit/browser/test_tab.py
Normal file
101
tests/unit/browser/test_tab.py
Normal 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
|
@ -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.')
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
||||
|
@ -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', [
|
||||
|
@ -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%]'
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -42,9 +42,6 @@ from qutebrowser.utils import objreg, qtutils
|
||||
from helpers import stubs
|
||||
|
||||
|
||||
Args = collections.namedtuple('Args', 'basedir')
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('qapp')
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user