Merge branch 'master' of github.com:The-Compiler/qutebrowser
This commit is contained in:
commit
402aa66756
13
.flake8
13
.flake8
@ -1,13 +0,0 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[flake8]
|
||||
# E265: Block comment should start with '#'
|
||||
# E501: Line too long
|
||||
# F841: unused variable
|
||||
# F401: Unused import
|
||||
# E402: module level import not at top of file
|
||||
# E266: too many leading '#' for block comment
|
||||
# W503: line break before binary operator
|
||||
ignore=E265,E501,F841,F401,E402,E266,W503
|
||||
max_complexity = 12
|
||||
exclude=resources.py
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ __pycache__
|
||||
/htmlcov
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
|
@ -4,7 +4,6 @@
|
||||
ignore=resources.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=pylint_checkers.config,
|
||||
pylint_checkers.crlf,
|
||||
pylint_checkers.modeline,
|
||||
pylint_checkers.openencoding,
|
||||
pylint_checkers.settrace
|
||||
|
@ -33,6 +33,11 @@ Added
|
||||
- New argument `--no-err-windows` to suppress all error windows.
|
||||
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
|
||||
- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is.
|
||||
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
|
||||
- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar.
|
||||
- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -41,7 +46,7 @@ Changed
|
||||
- `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior.
|
||||
- The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*.
|
||||
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
|
||||
- `<Ctrl-M>` is now accepted as an additional alias for `<Return>`/`<Ctrl-J>`
|
||||
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
|
||||
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
|
||||
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
|
||||
|
||||
@ -75,6 +80,8 @@ Fixed
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug)
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed exception when starting qutebrowser with `:set` as argument.
|
||||
- Fixed horrible completion performance when the `shrink` option was set.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
@ -91,9 +91,10 @@ unittests and several linters/checkers.
|
||||
|
||||
Currently, the following tools will be invoked when you run `tox`:
|
||||
|
||||
* Unit tests using the Python
|
||||
https://docs.python.org/3.4/library/unittest.html[unittest] framework
|
||||
* https://pypi.python.org/pypi/flake8/[flake8]
|
||||
* Unit tests using https://www.pytest.org[pytest].
|
||||
* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes]
|
||||
* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8]
|
||||
* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe]
|
||||
* https://github.com/GreenSteam/pep257/[pep257]
|
||||
* http://pylint.org/[pylint]
|
||||
* https://pypi.python.org/pypi/pyroma/[pyroma]
|
||||
|
@ -28,7 +28,6 @@ include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
exclude .flake8
|
||||
exclude .pylintrc
|
||||
exclude .eslintrc
|
||||
exclude doc/help
|
||||
|
@ -138,13 +138,14 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Claude
|
||||
* Martin Tournoij
|
||||
* Artur Shaik
|
||||
* Antoni Boucher
|
||||
* ZDarian
|
||||
* Peter Vilim
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Jimmy
|
||||
* Zach-Button
|
||||
* Martin Tournoij
|
||||
* rikn00
|
||||
* Patric Schmitz
|
||||
* Martin Zimmermann
|
||||
|
@ -642,13 +642,14 @@ Save open pages and quit.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*]+
|
||||
Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
@ -692,6 +693,7 @@ How many steps to zoom out.
|
||||
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||
|<<follow-selected,follow-selected>>|Follow the selected text.
|
||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||
|<<message-error,message-error>>|Show an error message in the statusbar.
|
||||
|<<message-info,message-info>>|Show an info message in the statusbar.
|
||||
@ -774,6 +776,15 @@ Enter a key mode.
|
||||
=== follow-hint
|
||||
Follow the currently selected hint.
|
||||
|
||||
[[follow-selected]]
|
||||
=== follow-selected
|
||||
Syntax: +:follow-selected [*--tab*]+
|
||||
|
||||
Follow the selected text.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
|
||||
|
||||
[[leave-mode]]
|
||||
=== leave-mode
|
||||
Leave the mode we're currently in.
|
||||
@ -1009,7 +1020,7 @@ multiplier
|
||||
|
||||
[[scroll-page]]
|
||||
=== scroll-page
|
||||
Syntax: +:scroll-page 'x' 'y'+
|
||||
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
|
||||
|
||||
Scroll the frame page-wise.
|
||||
|
||||
@ -1017,6 +1028,12 @@ Scroll the frame page-wise.
|
||||
* +'x'+: How many pages to scroll to the right.
|
||||
* +'y'+: How many pages to scroll down.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
|
||||
|
||||
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
|
||||
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
|
@ -45,6 +45,7 @@
|
||||
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
||||
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
||||
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|
||||
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``network''
|
||||
@ -220,6 +221,7 @@
|
||||
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
|
||||
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|
||||
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
|
||||
|<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``fonts''
|
||||
@ -594,6 +596,17 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-modal-js-dialog]]
|
||||
=== modal-js-dialog
|
||||
Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
== network
|
||||
Settings related to the network.
|
||||
|
||||
@ -1740,6 +1753,12 @@ Background color for downloads with errors.
|
||||
|
||||
Default: +pass:[red]+
|
||||
|
||||
[[colors-webpage.bg]]
|
||||
=== webpage.bg
|
||||
Background color for webpages if unset (or empty to use the theme's color)
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
== fonts
|
||||
Fonts used for the UI, with optional style/weight/size.
|
||||
|
||||
|
@ -606,7 +606,7 @@ class Quitter:
|
||||
# event loop, so we can shut down immediately.
|
||||
self._shutdown(status)
|
||||
|
||||
def _shutdown(self, status): # noqa
|
||||
def _shutdown(self, status):
|
||||
"""Second stage of shutdown."""
|
||||
log.destroy.debug("Stage 2 of shutting down...")
|
||||
if qApp is None:
|
||||
|
@ -25,7 +25,9 @@ import shlex
|
||||
import subprocess
|
||||
import posixpath
|
||||
import functools
|
||||
import xml.etree.ElementTree
|
||||
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWidgets import QApplication, QTabBar
|
||||
from PyQt5.QtCore import Qt, QUrl, QEvent
|
||||
from PyQt5.QtGui import QClipboard, QKeyEvent
|
||||
@ -643,14 +645,37 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
scope='window', count='count')
|
||||
def scroll_page(self, x: {'type': float}, y: {'type': float}, count=1):
|
||||
def scroll_page(self, x: {'type': float}, y: {'type': float}, *,
|
||||
top_navigate: {'type': ('prev', 'decrement'),
|
||||
'metavar': 'ACTION'}=None,
|
||||
bottom_navigate: {'type': ('next', 'increment'),
|
||||
'metavar': 'ACTION'}=None,
|
||||
count=1):
|
||||
"""Scroll the frame page-wise.
|
||||
|
||||
Args:
|
||||
x: How many pages to scroll to the right.
|
||||
y: How many pages to scroll down.
|
||||
bottom_navigate: :navigate action (next, increment) to run when
|
||||
scrolling down at the bottom of the page.
|
||||
top_navigate: :navigate action (prev, decrement) to run when
|
||||
scrolling up at the top of the page.
|
||||
count: multiplier
|
||||
"""
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
if not frame.url().isValid():
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/701
|
||||
return
|
||||
|
||||
if (bottom_navigate is not None and
|
||||
frame.scrollPosition().y() >=
|
||||
frame.scrollBarMaximum(Qt.Vertical)):
|
||||
self.navigate(bottom_navigate)
|
||||
return
|
||||
elif top_navigate is not None and frame.scrollPosition().y() == 0:
|
||||
self.navigate(top_navigate)
|
||||
return
|
||||
|
||||
mult_x = count * x
|
||||
mult_y = count * y
|
||||
if mult_y.is_integer():
|
||||
@ -663,7 +688,6 @@ class CommandDispatcher:
|
||||
mult_y = 0
|
||||
if mult_x == 0 and mult_y == 0:
|
||||
return
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
size = frame.geometry()
|
||||
dx = mult_x * size.width()
|
||||
dy = mult_y * size.height()
|
||||
@ -672,19 +696,28 @@ class CommandDispatcher:
|
||||
frame.scroll(dx, dy)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def yank(self, title=False, sel=False):
|
||||
def yank(self, title=False, sel=False, domain=False):
|
||||
"""Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
title: Yank the title instead of the URL.
|
||||
domain: Yank only the scheme, domain, and port number.
|
||||
"""
|
||||
clipboard = QApplication.clipboard()
|
||||
if title:
|
||||
s = self._tabbed_browser.page_title(self._current_index())
|
||||
what = 'title'
|
||||
elif domain:
|
||||
port = self._current_url().port()
|
||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||
self._current_url().host(),
|
||||
':' + str(port) if port > -1 else '')
|
||||
what = 'domain'
|
||||
else:
|
||||
s = self._current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
what = 'URL'
|
||||
if sel and clipboard.supportsSelection():
|
||||
mode = QClipboard.Selection
|
||||
target = "primary selection"
|
||||
@ -693,8 +726,8 @@ class CommandDispatcher:
|
||||
target = "clipboard"
|
||||
log.misc.debug("Yanking to {}: '{}'".format(target, s))
|
||||
clipboard.setText(s, mode)
|
||||
what = 'Title' if title else 'URL'
|
||||
message.info(self._win_id, "{} yanked to {}".format(what, target))
|
||||
message.info(self._win_id, "Yanked {} to {}: {}".format(
|
||||
what, target, s))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
count='count')
|
||||
@ -985,6 +1018,39 @@ class CommandDispatcher:
|
||||
url = objreg.get('quickmark-manager').get(name)
|
||||
self._open(url, tab, bg, window)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
scope='window')
|
||||
def follow_selected(self, tab=False):
|
||||
"""Follow the selected text.
|
||||
|
||||
Args:
|
||||
tab: Load the selected link in a new tab.
|
||||
"""
|
||||
widget = self._current_widget()
|
||||
page = widget.page()
|
||||
if not page.hasSelection():
|
||||
return
|
||||
if QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
if tab:
|
||||
page.open_target = usertypes.ClickTarget.tab
|
||||
page.currentFrame().evaluateJavaScript(
|
||||
'window.getSelection().anchorNode.parentNode.click()')
|
||||
else:
|
||||
try:
|
||||
selected_element = xml.etree.ElementTree.fromstring(
|
||||
'<html>' + widget.selectedHtml() + '</html>').find('a')
|
||||
except xml.etree.ElementTree.ParseError:
|
||||
raise cmdexc.CommandError('Could not parse selected element!')
|
||||
|
||||
if selected_element is not None:
|
||||
try:
|
||||
url = selected_element.attrib['href']
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError('Anchor element without href!')
|
||||
url = self._current_url().resolved(QUrl(url))
|
||||
self._open(url, tab)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='inspector',
|
||||
scope='window')
|
||||
def toggle_inspector(self):
|
||||
|
@ -181,7 +181,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
request.deleteLater()
|
||||
self.shutting_down.emit()
|
||||
|
||||
if SSL_AVAILABLE: # noqa
|
||||
if SSL_AVAILABLE: # pragma: no mccabe
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors):
|
||||
"""Decide if SSL errors should be ignored or not.
|
||||
|
@ -34,6 +34,7 @@ import configparser
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.browser.network import schemehandler, networkreply
|
||||
@ -96,6 +97,12 @@ class JSBridge(QObject):
|
||||
@pyqtSlot(int, str, str, str)
|
||||
def set(self, win_id, sectname, optname, value):
|
||||
"""Slot to set a setting from qute:settings."""
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/727
|
||||
if (sectname, optname == 'content', 'allow-javascript' and
|
||||
value == 'false'):
|
||||
message.error(win_id, "Refusing to disable javascript via "
|
||||
"qute:settings as it needs javascript support.")
|
||||
return
|
||||
try:
|
||||
objreg.get('config').set('conf', sectname, optname, value)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
@ -172,6 +179,14 @@ def qute_help(win_id, request):
|
||||
|
||||
def qute_settings(win_id, _request):
|
||||
"""Handler for qute:settings. View/change qute configuration."""
|
||||
if not QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/727
|
||||
template = jinja.env.get_template('pre.html')
|
||||
html = template.render(
|
||||
title='Failed to open qute:settings.',
|
||||
content="qute:settings needs javascript enabled to work.")
|
||||
else:
|
||||
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||
html = jinja.env.get_template('settings.html').render(
|
||||
win_id=win_id, title='settings', config=configdata,
|
||||
|
@ -478,17 +478,23 @@ class BrowserPage(QWebPage):
|
||||
return super().extension(ext, opt, out)
|
||||
return handler(opt, out)
|
||||
|
||||
def javaScriptAlert(self, _frame, msg):
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
"""Override javaScriptAlert to use the statusbar."""
|
||||
log.js.debug("alert: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptAlert(frame, msg)
|
||||
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-alert')):
|
||||
return
|
||||
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
||||
|
||||
def javaScriptConfirm(self, _frame, msg):
|
||||
def javaScriptConfirm(self, frame, msg):
|
||||
"""Override javaScriptConfirm to use the statusbar."""
|
||||
log.js.debug("confirm: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptConfirm(frame, msg)
|
||||
|
||||
if self._is_shutting_down:
|
||||
return False
|
||||
ans = self._ask("[js confirm] {}".format(msg),
|
||||
|
@ -24,6 +24,7 @@ import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
@ -108,6 +109,7 @@ class WebView(QWebView):
|
||||
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
|
||||
@ -181,6 +183,15 @@ class WebView(QWebView):
|
||||
self.load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
def _set_bg_color(self):
|
||||
"""Set the webpage background color as configured."""
|
||||
col = config.get('colors', 'webpage.bg')
|
||||
palette = self.palette()
|
||||
if col is None:
|
||||
col = self.style().standardPalette().color(QPalette.Base)
|
||||
palette.setColor(QPalette.Base, col)
|
||||
self.setPalette(palette)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Reinitialize the zoom neighborlist if related config changed."""
|
||||
@ -195,6 +206,8 @@ class WebView(QWebView):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
else:
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def init_neighborlist(self):
|
||||
"""Initialize the _zoom neighborlist."""
|
||||
@ -607,6 +620,7 @@ class WebView(QWebView):
|
||||
"""Save a reference to the context menu so we can close it."""
|
||||
menu = self.page().createStandardContextMenu()
|
||||
self.shutting_down.connect(menu.close)
|
||||
modeman.instance(self.win_id).entered.connect(menu.close)
|
||||
menu.exec_(e.globalPos())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
|
@ -61,7 +61,8 @@ class Command:
|
||||
"""
|
||||
|
||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||
['kwargs', 'type', 'flag', 'hide'])
|
||||
['kwargs', 'type', 'flag', 'hide',
|
||||
'metavar'])
|
||||
|
||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||
hide=False, completion=None, modes=None, not_modes=None,
|
||||
@ -257,10 +258,10 @@ class Command:
|
||||
pass
|
||||
|
||||
if isinstance(typ, tuple):
|
||||
pass
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
elif utils.is_enum(typ):
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
kwargs['metavar'] = param.name
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
elif typ is bool:
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
@ -322,11 +323,12 @@ class Command:
|
||||
flag: The short name/flag if overridden.
|
||||
name: The long name if overridden.
|
||||
"""
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False}
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
|
||||
'metavar': None}
|
||||
if param.annotation is not inspect.Parameter.empty:
|
||||
log.commands.vdebug("Parsing annotation {}".format(
|
||||
param.annotation))
|
||||
for field in ('type', 'flag', 'name', 'hide'):
|
||||
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
|
||||
if field in param.annotation:
|
||||
info[field] = param.annotation[field]
|
||||
if 'nargs' in param.annotation:
|
||||
@ -418,7 +420,7 @@ class Command:
|
||||
value = self._type_conv[param.name](value)
|
||||
return name, value
|
||||
|
||||
def _get_call_args(self, win_id): # noqa
|
||||
def _get_call_args(self, win_id):
|
||||
"""Get arguments for a function call.
|
||||
|
||||
Args:
|
||||
|
@ -148,7 +148,7 @@ class _BaseUserscriptRunner(QObject):
|
||||
def run(self, cmd, *args, env=None):
|
||||
"""Run the userscript given.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
Needs to be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
@ -160,7 +160,7 @@ class _BaseUserscriptRunner(QObject):
|
||||
def on_proc_finished(self):
|
||||
"""Called when the process has finished.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
Needs to be overridden by subclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -272,7 +272,7 @@ class Completer(QObject):
|
||||
pattern = parts[self._cursor_part].strip()
|
||||
except IndexError:
|
||||
pattern = ''
|
||||
self._model().set_pattern(pattern)
|
||||
completion.set_pattern(pattern)
|
||||
|
||||
log.completion.debug(
|
||||
"New completion for {}: {}, with pattern '{}'".format(
|
||||
|
@ -201,8 +201,17 @@ class CompletionView(QTreeView):
|
||||
for i in range(model.rowCount()):
|
||||
self.expand(model.index(i, 0))
|
||||
self._resize_columns()
|
||||
model.rowsRemoved.connect(self.maybe_resize_completion)
|
||||
model.rowsInserted.connect(self.maybe_resize_completion)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
"""Set the completion pattern for the current model.
|
||||
|
||||
Called from on_update_completion().
|
||||
|
||||
Args:
|
||||
pattern: The filter pattern to set (what the user entered).
|
||||
"""
|
||||
self.model().set_pattern(pattern)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -305,6 +305,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to hide the mouse cursor."),
|
||||
|
||||
('modal-js-dialog',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Use standard JavaScript modal dialog for alert() and confirm()"),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@ -955,6 +959,11 @@ def data(readonly=False):
|
||||
SettingValue(typ.QtColor(), 'red'),
|
||||
"Background color for downloads with errors."),
|
||||
|
||||
('webpage.bg',
|
||||
SettingValue(typ.QtColor(none_ok=True), 'white'),
|
||||
"Background color for webpages if unset (or empty to use the "
|
||||
"theme's color)"),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@ -1121,6 +1130,12 @@ KEY_SECTION_DESC = {
|
||||
""),
|
||||
}
|
||||
|
||||
# Keys which are similar to Return and should be bound by default where Return
|
||||
# is bound.
|
||||
|
||||
RETURN_KEYS = ['<Return>', '<Ctrl-M>', '<Ctrl-J>', '<Shift-Return>', '<Enter>',
|
||||
'<Shift-Enter>']
|
||||
|
||||
|
||||
KEY_DATA = collections.OrderedDict([
|
||||
('!normal', collections.OrderedDict([
|
||||
@ -1189,6 +1204,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('yank -s', ['yY']),
|
||||
('yank -t', ['yt']),
|
||||
('yank -ts', ['yT']),
|
||||
('yank -d', ['yd']),
|
||||
('yank -ds', ['yD']),
|
||||
('paste', ['pp']),
|
||||
('paste -s', ['pP']),
|
||||
('paste -t', ['Pp']),
|
||||
@ -1239,6 +1256,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('stop', ['<Ctrl-s>']),
|
||||
('print', ['<Ctrl-Alt-p>']),
|
||||
('open qute:settings', ['Ss']),
|
||||
('follow-selected', RETURN_KEYS),
|
||||
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
||||
])),
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
@ -1246,7 +1265,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
])),
|
||||
|
||||
('hint', collections.OrderedDict([
|
||||
('follow-hint', ['<Return>', '<Ctrl-M>', '<Ctrl-J>']),
|
||||
('follow-hint', RETURN_KEYS),
|
||||
('hint --rapid links tab-bg', ['<Ctrl-R>']),
|
||||
('hint links', ['<Ctrl-F>']),
|
||||
('hint all tab-bg', ['<Ctrl-B>']),
|
||||
@ -1259,13 +1278,11 @@ KEY_DATA = collections.OrderedDict([
|
||||
('command-history-next', ['<Ctrl-N>']),
|
||||
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-next', ['<Tab>', '<Down>']),
|
||||
('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>',
|
||||
'<Ctrl-M>']),
|
||||
('command-accept', RETURN_KEYS),
|
||||
])),
|
||||
|
||||
('prompt', collections.OrderedDict([
|
||||
('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>',
|
||||
'<Ctrl-M>']),
|
||||
('prompt-accept', RETURN_KEYS),
|
||||
('prompt-yes', ['y']),
|
||||
('prompt-no', ['n']),
|
||||
])),
|
||||
@ -1306,7 +1323,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('move-to-start-of-document', ['gg']),
|
||||
('move-to-end-of-document', ['G']),
|
||||
('yank-selected -p', ['Y']),
|
||||
('yank-selected', ['y', '<Return>', '<Ctrl-J>']),
|
||||
('yank-selected', ['y'] + RETURN_KEYS),
|
||||
('scroll left', ['H']),
|
||||
('scroll down', ['J']),
|
||||
('scroll up', ['K']),
|
||||
|
@ -694,7 +694,7 @@ class FontFamily(Font):
|
||||
|
||||
class QtFont(Font):
|
||||
|
||||
"""A Font which gets converted to q QFont."""
|
||||
"""A Font which gets converted to a QFont."""
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
|
@ -49,6 +49,8 @@ class BaseKeyParser(QObject):
|
||||
special: execute() was called via a special key binding
|
||||
|
||||
do_log: Whether to log keypresses or not.
|
||||
passthrough: Whether unbound keys should be passed through with this
|
||||
handler.
|
||||
|
||||
Attributes:
|
||||
bindings: Bound key bindings
|
||||
@ -69,6 +71,7 @@ class BaseKeyParser(QObject):
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
do_log = True
|
||||
passthrough = False
|
||||
|
||||
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
|
||||
'other', 'none'])
|
||||
|
@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
|
||||
"""
|
||||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, mode, parent=None, warn=True):
|
||||
"""Constructor.
|
||||
|
@ -84,38 +84,30 @@ def init(win_id, parent):
|
||||
modeman.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'keyparsers', scope='window',
|
||||
window=win_id))
|
||||
modeman.register(KM.normal, keyparsers[KM.normal].handle)
|
||||
modeman.register(KM.hint, keyparsers[KM.hint].handle)
|
||||
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
|
||||
modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.command, keyparsers[KM.command].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
|
||||
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
|
||||
modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True)
|
||||
for mode, parser in keyparsers.items():
|
||||
modeman.register(mode, parser)
|
||||
return modeman
|
||||
|
||||
|
||||
def _get_modeman(win_id):
|
||||
def instance(win_id):
|
||||
"""Get a modemanager object."""
|
||||
return objreg.get('mode-manager', scope='window', window=win_id)
|
||||
|
||||
|
||||
def enter(win_id, mode, reason=None, only_if_normal=False):
|
||||
"""Enter the mode 'mode'."""
|
||||
_get_modeman(win_id).enter(mode, reason, only_if_normal)
|
||||
instance(win_id).enter(mode, reason, only_if_normal)
|
||||
|
||||
|
||||
def leave(win_id, mode, reason=None):
|
||||
"""Leave the mode 'mode'."""
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
instance(win_id).leave(mode, reason)
|
||||
|
||||
|
||||
def maybe_leave(win_id, mode, reason=None):
|
||||
"""Convenience method to leave 'mode' without exceptions."""
|
||||
try:
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
instance(win_id).leave(mode, reason)
|
||||
except NotInModeError as e:
|
||||
# This is rather likely to happen, so we only log to debug log.
|
||||
log.modes.debug("{} (leave reason: {})".format(e, reason))
|
||||
@ -126,10 +118,9 @@ class ModeManager(QObject):
|
||||
"""Manager for keyboard modes.
|
||||
|
||||
Attributes:
|
||||
passthrough: A list of modes in which to pass through events.
|
||||
mode: The mode we're currently in.
|
||||
_win_id: The window ID of this ModeManager
|
||||
_handlers: A dictionary of modes and their handlers.
|
||||
_parsers: A dictionary of modes and their keyparsers.
|
||||
_forward_unbound_keys: If we should forward unbound keys.
|
||||
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
||||
passed through, so the release event should as
|
||||
@ -151,8 +142,7 @@ class ModeManager(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._handlers = {}
|
||||
self.passthrough = []
|
||||
self._parsers = {}
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass = set()
|
||||
self._forward_unbound_keys = config.get(
|
||||
@ -160,8 +150,7 @@ class ModeManager(QObject):
|
||||
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, mode=self.mode,
|
||||
passthrough=self.passthrough)
|
||||
return utils.get_repr(self, mode=self.mode)
|
||||
|
||||
def _eventFilter_keypress(self, event):
|
||||
"""Handle filtering of KeyPress events.
|
||||
@ -173,11 +162,11 @@ class ModeManager(QObject):
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
curmode = self.mode
|
||||
handler = self._handlers[curmode]
|
||||
parser = self._parsers[curmode]
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("got keypress in mode {} - calling handler "
|
||||
"{}".format(curmode, utils.qualname(handler)))
|
||||
handled = handler(event) if handler is not None else False
|
||||
log.modes.debug("got keypress in mode {} - delegating to "
|
||||
"{}".format(curmode, utils.qualname(parser)))
|
||||
handled = parser.handle(event)
|
||||
|
||||
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
@ -187,7 +176,7 @@ class ModeManager(QObject):
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (curmode in self.passthrough or
|
||||
elif (parser.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
filter_this = False
|
||||
@ -202,8 +191,8 @@ class ModeManager(QObject):
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
curmode in self.passthrough, is_non_alnum,
|
||||
is_tab, filter_this, focus_widget))
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
def _eventFilter_keyrelease(self, event):
|
||||
@ -226,20 +215,16 @@ class ModeManager(QObject):
|
||||
log.modes.debug("filter: {}".format(filter_this))
|
||||
return filter_this
|
||||
|
||||
def register(self, mode, handler, passthrough=False):
|
||||
def register(self, mode, parser):
|
||||
"""Register a new mode.
|
||||
|
||||
Args:
|
||||
mode: The name of the mode.
|
||||
handler: Handler for keyPressEvents.
|
||||
passthrough: Whether to pass key bindings in this mode through to
|
||||
the widgets.
|
||||
parser: The KeyParser which should be used.
|
||||
"""
|
||||
if not isinstance(mode, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
self._handlers[mode] = handler
|
||||
if passthrough:
|
||||
self.passthrough.append(mode)
|
||||
assert isinstance(mode, usertypes.KeyMode)
|
||||
assert parser is not None
|
||||
self._parsers[mode] = parser
|
||||
|
||||
def enter(self, mode, reason=None, only_if_normal=False):
|
||||
"""Enter a new mode.
|
||||
@ -253,8 +238,8 @@ class ModeManager(QObject):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
log.modes.debug("Entering mode {}{}".format(
|
||||
mode, '' if reason is None else ' (reason: {})'.format(reason)))
|
||||
if mode not in self._handlers:
|
||||
raise ValueError("No handler for mode {}".format(mode))
|
||||
if mode not in self._parsers:
|
||||
raise ValueError("No keyparser for mode {}".format(mode))
|
||||
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
|
||||
if self.mode == mode or (self.mode in prompt_modes and
|
||||
mode in prompt_modes):
|
||||
|
@ -224,6 +224,8 @@ class CaretKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
"""KeyParser for caret mode."""
|
||||
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True,
|
||||
supports_chains=True)
|
||||
|
@ -469,9 +469,9 @@ class StatusBar(QWidget):
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Mark certain modes in the commandline."""
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if mode in mode_manager.passthrough:
|
||||
if keyparsers[mode].passthrough:
|
||||
self._set_mode_text(mode.name)
|
||||
if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
|
||||
self.set_mode_active(mode, True)
|
||||
@ -479,10 +479,10 @@ class StatusBar(QWidget):
|
||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||
def on_mode_left(self, old_mode, new_mode):
|
||||
"""Clear marked mode."""
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if old_mode in mode_manager.passthrough:
|
||||
if new_mode in mode_manager.passthrough:
|
||||
if keyparsers[old_mode].passthrough:
|
||||
if keyparsers[new_mode].passthrough:
|
||||
self._set_mode_text(new_mode.name)
|
||||
else:
|
||||
self.txt.set_text(self.txt.Text.normal, '')
|
||||
|
@ -296,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab: True to open URL in a new tab, False otherwise.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
if newtab:
|
||||
if newtab or self.currentWidget() is None:
|
||||
self.tabopen(url, background=False)
|
||||
else:
|
||||
self.currentWidget().openurl(url)
|
||||
|
@ -183,7 +183,7 @@ class _CrashDialog(QDialog):
|
||||
def _init_text(self):
|
||||
"""Initialize the main text to be displayed on an exception.
|
||||
|
||||
Should be extended by superclass to set the actual text."""
|
||||
Should be extended by subclasses to set the actual text."""
|
||||
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
|
||||
textInteractionFlags=Qt.LinksAccessibleByMouse)
|
||||
self._vbox.addWidget(self._lbl)
|
||||
|
@ -190,7 +190,7 @@ class CrashHandler(QObject):
|
||||
objects = ""
|
||||
return ExceptionInfo(pages, cmd_history, objects)
|
||||
|
||||
def exception_hook(self, exctype, excvalue, tb): # noqa
|
||||
def exception_hook(self, exctype, excvalue, tb):
|
||||
"""Handle uncaught python exceptions.
|
||||
|
||||
It'll try very hard to write all open tabs to a file, and then exit
|
||||
|
@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit):
|
||||
def __on_cursor_position_changed(self, _old, new):
|
||||
"""Prevent the cursor moving to the prompt.
|
||||
|
||||
We use __ here to avoid accidentally overriding it in superclasses.
|
||||
We use __ here to avoid accidentally overriding it in subclasses.
|
||||
"""
|
||||
if new < self._promptlen:
|
||||
self.setCursorPosition(self._promptlen)
|
||||
|
@ -55,7 +55,7 @@ class ShellLexer:
|
||||
self.token = ''
|
||||
self.state = ' '
|
||||
|
||||
def __iter__(self): # noqa
|
||||
def __iter__(self): # pragma: no mccabe
|
||||
"""Read a raw token from the input stream."""
|
||||
# pylint: disable=too-many-branches,too-many-statements
|
||||
self.reset()
|
||||
|
@ -242,7 +242,7 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
|
||||
'err_config', 'err_key_config'], is_int=True)
|
||||
'err_config', 'err_key_config'], is_int=True, start=0)
|
||||
|
||||
|
||||
class Question(QObject):
|
||||
|
@ -127,18 +127,8 @@ def _module_versions():
|
||||
A list of lines with version info.
|
||||
"""
|
||||
lines = []
|
||||
try:
|
||||
import sipconfig # pylint: disable=import-error,unused-variable
|
||||
except ImportError:
|
||||
lines.append('SIP: ?')
|
||||
else:
|
||||
try:
|
||||
lines.append('SIP: {}'.format(
|
||||
sipconfig.Configuration().sip_version_str))
|
||||
except (AttributeError, TypeError):
|
||||
log.misc.exception("Error while getting SIP version")
|
||||
lines.append('SIP: ?')
|
||||
modules = collections.OrderedDict([
|
||||
('sip', ['SIP_VERSION_STR']),
|
||||
('colorlog', []),
|
||||
('colorama', ['VERSION', '__version__']),
|
||||
('pypeg2', ['__version__']),
|
||||
|
@ -1,45 +0,0 @@
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# 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/>.
|
||||
|
||||
"""Checker for CRLF in files."""
|
||||
|
||||
from pylint import interfaces, checkers
|
||||
|
||||
|
||||
class CrlfChecker(checkers.BaseChecker):
|
||||
|
||||
"""Check for CRLF in files."""
|
||||
|
||||
__implements__ = interfaces.IRawChecker
|
||||
|
||||
name = 'crlf'
|
||||
msgs = {'W9001': ('Uses CRLFs', 'crlf', None)}
|
||||
options = ()
|
||||
priority = -1
|
||||
|
||||
def process_module(self, node):
|
||||
"""Process the module."""
|
||||
for (lineno, line) in enumerate(node.file_stream):
|
||||
if b'\r\n' in line:
|
||||
self.add_message('crlf', line=lineno)
|
||||
return
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the checker."""
|
||||
linter.register_checker(CrlfChecker(linter))
|
@ -44,6 +44,9 @@ def progress_widget(qtbot, monkeypatch, config_stub):
|
||||
return widget
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason='Blacklisted because it could cause random segfaults - see '
|
||||
'https://github.com/hackebrot/qutebrowser/issues/22', run=False)
|
||||
def test_load_started(progress_widget):
|
||||
"""Ensure the Progress widget reacts properly when the page starts loading.
|
||||
|
||||
|
@ -21,144 +21,256 @@
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
import re
|
||||
import inspect
|
||||
from unittest import mock
|
||||
|
||||
from PyQt5.QtWidgets import QLineEdit
|
||||
from PyQt5.QtWidgets import QLineEdit, QApplication
|
||||
import pytest
|
||||
|
||||
from qutebrowser.misc import readline
|
||||
|
||||
|
||||
# Some functions aren't 100% readline compatible:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/678
|
||||
# Those are marked with fixme and have another value marked with '# wrong'
|
||||
# which marks the current behavior.
|
||||
|
||||
fixme = pytest.mark.xfail(reason='readline compatibility - see #678')
|
||||
|
||||
|
||||
class LineEdit(QLineEdit):
|
||||
|
||||
"""QLineEdit with some methods to make testing easier."""
|
||||
|
||||
def _get_index(self, haystack, needle):
|
||||
"""Get the index of a char (needle) in a string (haystack).
|
||||
|
||||
Return:
|
||||
The position where needle was found, or None if it wasn't found.
|
||||
"""
|
||||
try:
|
||||
return haystack.index(needle)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def set_aug_text(self, text):
|
||||
"""Set a text with </> markers for selected text and | as cursor."""
|
||||
real_text = re.sub('[<>|]', '', text)
|
||||
self.setText(real_text)
|
||||
|
||||
cursor_pos = self._get_index(text, '|')
|
||||
sel_start_pos = self._get_index(text, '<')
|
||||
sel_end_pos = self._get_index(text, '>')
|
||||
|
||||
if sel_start_pos is not None and sel_end_pos is None:
|
||||
raise ValueError("< given without >!")
|
||||
if sel_start_pos is None and sel_end_pos is not None:
|
||||
raise ValueError("> given without <!")
|
||||
|
||||
if cursor_pos is not None:
|
||||
if sel_start_pos is not None or sel_end_pos is not None:
|
||||
raise ValueError("Can't mix | and </>!")
|
||||
self.setCursorPosition(cursor_pos)
|
||||
elif sel_start_pos is not None:
|
||||
if sel_start_pos > sel_end_pos:
|
||||
raise ValueError("< given after >!")
|
||||
sel_len = sel_end_pos - sel_start_pos - 1
|
||||
self.setSelection(sel_start_pos, sel_len)
|
||||
|
||||
def aug_text(self):
|
||||
"""Get a text with </> markers for selected text and | as cursor."""
|
||||
text = self.text()
|
||||
chars = list(text)
|
||||
cur_pos = self.cursorPosition()
|
||||
assert cur_pos >= 0
|
||||
chars.insert(cur_pos, '|')
|
||||
if self.hasSelectedText():
|
||||
selected_text = self.selectedText()
|
||||
sel_start = self.selectionStart()
|
||||
sel_end = sel_start + len(selected_text)
|
||||
assert sel_start > 0
|
||||
assert sel_end > 0
|
||||
assert sel_end > sel_start
|
||||
assert cur_pos == sel_end
|
||||
assert text[sel_start:sel_end] == selected_text
|
||||
chars.insert(sel_start, '<')
|
||||
chars.insert(sel_end + 1, '>')
|
||||
return ''.join(chars)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_qapp(monkeypatch, stubs):
|
||||
"""Fixture that mocks readline.QApplication and returns it."""
|
||||
stub = stubs.FakeQApplication()
|
||||
monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub)
|
||||
return stub
|
||||
def lineedit(qtbot, monkeypatch):
|
||||
"""Fixture providing a LineEdit."""
|
||||
le = LineEdit()
|
||||
qtbot.add_widget(le)
|
||||
monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le)
|
||||
return le
|
||||
|
||||
|
||||
class TestNoneWidget:
|
||||
@pytest.fixture
|
||||
def bridge():
|
||||
"""Fixture providing a ReadlineBridge."""
|
||||
return readline.ReadlineBridge()
|
||||
|
||||
"""Test if there are no exceptions when the widget is None."""
|
||||
|
||||
def test_none(self, mocked_qapp):
|
||||
def test_none(bridge, qtbot):
|
||||
"""Call each rl_* method with a None focusWidget."""
|
||||
self.bridge = readline.ReadlineBridge()
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=None)
|
||||
for name, method in inspect.getmembers(self.bridge, inspect.ismethod):
|
||||
assert QApplication.instance().focusWidget() is None
|
||||
for name, method in inspect.getmembers(bridge, inspect.ismethod):
|
||||
if name.startswith('rl_'):
|
||||
method()
|
||||
|
||||
|
||||
class TestReadlineBridgeTest:
|
||||
|
||||
"""Tests for readline bridge."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.qle = mock.Mock()
|
||||
self.qle.__class__ = QLineEdit
|
||||
self.bridge = readline.ReadlineBridge()
|
||||
|
||||
def _set_selected_text(self, text):
|
||||
"""Set the value the fake QLineEdit should return for selectedText."""
|
||||
self.qle.configure_mock(**{'selectedText.return_value': text})
|
||||
|
||||
def test_rl_backward_char(self, mocked_qapp):
|
||||
@pytest.mark.parametrize('text, expected', [('f<oo>bar', 'fo|obar'),
|
||||
('|foobar', '|foobar')])
|
||||
def test_rl_backward_char(text, expected, lineedit, bridge):
|
||||
"""Test rl_backward_char."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_backward_char()
|
||||
self.qle.cursorBackward.assert_called_with(False)
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_backward_char()
|
||||
assert lineedit.aug_text() == expected
|
||||
|
||||
def test_rl_forward_char(self, mocked_qapp):
|
||||
|
||||
@pytest.mark.parametrize('text, expected', [('f<oo>bar', 'foob|ar'),
|
||||
('foobar|', 'foobar|')])
|
||||
def test_rl_forward_char(text, expected, lineedit, bridge):
|
||||
"""Test rl_forward_char."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_forward_char()
|
||||
self.qle.cursorForward.assert_called_with(False)
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_forward_char()
|
||||
assert lineedit.aug_text() == expected
|
||||
|
||||
def test_rl_backward_word(self, mocked_qapp):
|
||||
|
||||
@pytest.mark.parametrize('text, expected', [('one <tw>o', 'one |two'),
|
||||
('<one >two', '|one two'),
|
||||
('|one two', '|one two')])
|
||||
def test_rl_backward_word(text, expected, lineedit, bridge):
|
||||
"""Test rl_backward_word."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_backward_word()
|
||||
self.qle.cursorWordBackward.assert_called_with(False)
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_backward_word()
|
||||
assert lineedit.aug_text() == expected
|
||||
|
||||
def test_rl_forward_word(self, mocked_qapp):
|
||||
|
||||
@pytest.mark.parametrize('text, expected', [
|
||||
fixme(('<o>ne two', 'one| two')),
|
||||
('<o>ne two', 'one |two'), # wrong
|
||||
fixme(('<one> two', 'one two|')),
|
||||
('<one> two', 'one |two'), # wrong
|
||||
('one t<wo>', 'one two|')
|
||||
])
|
||||
def test_rl_forward_word(text, expected, lineedit, bridge):
|
||||
"""Test rl_forward_word."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_forward_word()
|
||||
self.qle.cursorWordForward.assert_called_with(False)
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_forward_word()
|
||||
assert lineedit.aug_text() == expected
|
||||
|
||||
def test_rl_beginning_of_line(self, mocked_qapp):
|
||||
|
||||
def test_rl_beginning_of_line(lineedit, bridge):
|
||||
"""Test rl_beginning_of_line."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_beginning_of_line()
|
||||
self.qle.home.assert_called_with(False)
|
||||
lineedit.set_aug_text('f<oo>bar')
|
||||
bridge.rl_beginning_of_line()
|
||||
assert lineedit.aug_text() == '|foobar'
|
||||
|
||||
def test_rl_end_of_line(self, mocked_qapp):
|
||||
|
||||
def test_rl_end_of_line(lineedit, bridge):
|
||||
"""Test rl_end_of_line."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_end_of_line()
|
||||
self.qle.end.assert_called_with(False)
|
||||
lineedit.set_aug_text('f<oo>bar')
|
||||
bridge.rl_end_of_line()
|
||||
assert lineedit.aug_text() == 'foobar|'
|
||||
|
||||
def test_rl_delete_char(self, mocked_qapp):
|
||||
|
||||
@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'),
|
||||
('foobar|', 'foobar|'),
|
||||
('|foobar', '|oobar'),
|
||||
('f<oo>bar', 'f|bar')])
|
||||
def test_rl_delete_char(text, expected, lineedit, bridge):
|
||||
"""Test rl_delete_char."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_delete_char()
|
||||
self.qle.del_.assert_called_with()
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_delete_char()
|
||||
assert lineedit.aug_text() == expected
|
||||
|
||||
def test_rl_backward_delete_char(self, mocked_qapp):
|
||||
|
||||
@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'),
|
||||
('foobar|', 'fooba|'),
|
||||
('|foobar', '|foobar'),
|
||||
('f<oo>bar', 'f|bar')])
|
||||
def test_rl_backward_delete_char(text, expected, lineedit, bridge):
|
||||
"""Test rl_backward_delete_char."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_backward_delete_char()
|
||||
self.qle.backspace.assert_called_with()
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_backward_delete_char()
|
||||
assert lineedit.aug_text() == expected
|
||||
|
||||
def test_rl_unix_line_discard(self, mocked_qapp):
|
||||
"""Set a selected text, delete it, see if it comes back with yank."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self._set_selected_text("delete test")
|
||||
self.bridge.rl_unix_line_discard()
|
||||
self.qle.home.assert_called_with(True)
|
||||
assert self.bridge._deleted[self.qle] == "delete test"
|
||||
self.qle.del_.assert_called_with()
|
||||
self.bridge.rl_yank()
|
||||
self.qle.insert.assert_called_with("delete test")
|
||||
|
||||
def test_rl_kill_line(self, mocked_qapp):
|
||||
"""Set a selected text, delete it, see if it comes back with yank."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self._set_selected_text("delete test")
|
||||
self.bridge.rl_kill_line()
|
||||
self.qle.end.assert_called_with(True)
|
||||
assert self.bridge._deleted[self.qle] == "delete test"
|
||||
self.qle.del_.assert_called_with()
|
||||
self.bridge.rl_yank()
|
||||
self.qle.insert.assert_called_with("delete test")
|
||||
@pytest.mark.parametrize('text, deleted, rest', [
|
||||
('delete this| test', 'delete this', '| test'),
|
||||
fixme(('delete <this> test', 'delete this', '| test')),
|
||||
('delete <this> test', 'delete ', '|this test'), # wrong
|
||||
fixme(('f<oo>bar', 'foo', '|bar')),
|
||||
('f<oo>bar', 'f', '|oobar'), # wrong
|
||||
])
|
||||
def test_rl_unix_line_discard(lineedit, bridge, text, deleted, rest):
|
||||
"""Delete from the cursor to the beginning of the line and yank back."""
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_unix_line_discard()
|
||||
assert bridge._deleted[lineedit] == deleted
|
||||
assert lineedit.aug_text() == rest
|
||||
lineedit.clear()
|
||||
bridge.rl_yank()
|
||||
assert lineedit.aug_text() == deleted + '|'
|
||||
|
||||
def test_rl_unix_word_rubout(self, mocked_qapp):
|
||||
"""Set a selected text, delete it, see if it comes back with yank."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self._set_selected_text("delete test")
|
||||
self.bridge.rl_unix_word_rubout()
|
||||
self.qle.cursorWordBackward.assert_called_with(True)
|
||||
assert self.bridge._deleted[self.qle] == "delete test"
|
||||
self.qle.del_.assert_called_with()
|
||||
self.bridge.rl_yank()
|
||||
self.qle.insert.assert_called_with("delete test")
|
||||
|
||||
def test_rl_kill_word(self, mocked_qapp):
|
||||
"""Set a selected text, delete it, see if it comes back with yank."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self._set_selected_text("delete test")
|
||||
self.bridge.rl_kill_word()
|
||||
self.qle.cursorWordForward.assert_called_with(True)
|
||||
assert self.bridge._deleted[self.qle] == "delete test"
|
||||
self.qle.del_.assert_called_with()
|
||||
self.bridge.rl_yank()
|
||||
self.qle.insert.assert_called_with("delete test")
|
||||
@pytest.mark.parametrize('text, deleted, rest', [
|
||||
('test |delete this', 'delete this', 'test |'),
|
||||
fixme(('<test >delete this', 'test delete this', 'test |')),
|
||||
('<test >delete this', 'test delete this', '|'), # wrong
|
||||
])
|
||||
def test_rl_kill_line(lineedit, bridge, text, deleted, rest):
|
||||
"""Delete from the cursor to the end of line and yank back."""
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_kill_line()
|
||||
assert bridge._deleted[lineedit] == deleted
|
||||
assert lineedit.aug_text() == rest
|
||||
lineedit.clear()
|
||||
bridge.rl_yank()
|
||||
assert lineedit.aug_text() == deleted + '|'
|
||||
|
||||
def test_rl_yank_no_text(self, mocked_qapp):
|
||||
|
||||
@pytest.mark.parametrize('text, deleted, rest', [
|
||||
('test delete|foobar', 'delete', 'test |foobar'),
|
||||
('test delete |foobar', 'delete ', 'test |foobar'),
|
||||
fixme(('test del<ete>foobar', 'delete', 'test |foobar')),
|
||||
('test del<ete >foobar', 'del', 'test |ete foobar'), # wrong
|
||||
])
|
||||
def test_rl_unix_word_rubout(lineedit, bridge, text, deleted, rest):
|
||||
"""Delete to word beginning and see if it comes back with yank."""
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_unix_word_rubout()
|
||||
assert bridge._deleted[lineedit] == deleted
|
||||
assert lineedit.aug_text() == rest
|
||||
lineedit.clear()
|
||||
bridge.rl_yank()
|
||||
assert lineedit.aug_text() == deleted + '|'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('text, deleted, rest', [
|
||||
fixme(('test foobar| delete', ' delete', 'test foobar|')),
|
||||
('test foobar| delete', ' ', 'test foobar|delete'), # wrong
|
||||
fixme(('test foo|delete bar', 'delete', 'test foo| bar')),
|
||||
('test foo|delete bar', 'delete ', 'test foo|bar'), # wrong
|
||||
fixme(('test foo<bar> delete', ' delete', 'test foobar|')),
|
||||
('test foo<bar>delete', 'bardelete', 'test foo|'), # wrong
|
||||
])
|
||||
def test_rl_kill_word(lineedit, bridge, text, deleted, rest):
|
||||
"""Delete to word end and see if it comes back with yank."""
|
||||
lineedit.set_aug_text(text)
|
||||
bridge.rl_kill_word()
|
||||
assert bridge._deleted[lineedit] == deleted
|
||||
assert lineedit.aug_text() == rest
|
||||
lineedit.clear()
|
||||
bridge.rl_yank()
|
||||
assert lineedit.aug_text() == deleted + '|'
|
||||
|
||||
|
||||
def test_rl_yank_no_text(lineedit, bridge):
|
||||
"""Test yank without having deleted anything."""
|
||||
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
|
||||
self.bridge.rl_yank()
|
||||
assert not self.qle.insert.called
|
||||
lineedit.clear()
|
||||
bridge.rl_yank()
|
||||
assert lineedit.aug_text() == '|'
|
||||
|
@ -54,3 +54,9 @@ def test_start():
|
||||
e = usertypes.enum('Enum', ['three', 'four'], start=3)
|
||||
assert e.three.value == 3
|
||||
assert e.four.value == 4
|
||||
|
||||
|
||||
def test_exit():
|
||||
"""Make sure the exit status enum is correct."""
|
||||
assert usertypes.Exit.ok == 0
|
||||
assert usertypes.Exit.reserved == 1
|
||||
|
54
tox.ini
54
tox.ini
@ -4,7 +4,7 @@
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest
|
||||
envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
@ -61,8 +61,8 @@ deps =
|
||||
six==1.9.0
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no
|
||||
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no
|
||||
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
||||
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
||||
|
||||
[testenv:pep257]
|
||||
skip_install = true
|
||||
@ -74,16 +74,40 @@ passenv = LANG
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_content_disposition).*\.py'
|
||||
|
||||
[testenv:flake8]
|
||||
skip_install = true
|
||||
[testenv:pyflakes]
|
||||
# https://github.com/fschulze/pytest-flakes/issues/6
|
||||
setenv = LANG=en_US.UTF-8
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
pyflakes==0.8.1
|
||||
pep8==1.5.7 # rq.filter: <1.6.0
|
||||
flake8==2.4.0
|
||||
py==1.4.27
|
||||
pytest==2.7.1
|
||||
pyflakes==0.9.0
|
||||
pytest-flakes==0.2
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envdir}/bin/flake8 scripts tests qutebrowser --config=.flake8
|
||||
{envpython} -m py.test -q --flakes -m flakes
|
||||
|
||||
[testenv:pep8]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
py==1.4.27
|
||||
pytest==2.7.1
|
||||
pep8==1.6.2
|
||||
pytest-pep8==1.0.6
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envpython} -m py.test -q --pep8 -m pep8
|
||||
|
||||
[testenv:mccabe]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
py==1.4.27
|
||||
pytest==2.7.1
|
||||
mccabe==0.3
|
||||
pytest-mccabe==0.1
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envpython} -m py.test -q --mccabe -m mccabe
|
||||
|
||||
[testenv:pyroma]
|
||||
skip_install = true
|
||||
@ -129,3 +153,15 @@ commands =
|
||||
norecursedirs = .tox .venv
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
flakes-ignore =
|
||||
UnusedImport
|
||||
UnusedVariable
|
||||
resources.py ALL
|
||||
pep8ignore =
|
||||
E265 # Block comment should start with '#'
|
||||
E501 # Line too long
|
||||
E402 # module level import not at top of file
|
||||
E266 # too many leading '#' for block comment
|
||||
W503 # line break before binary operator
|
||||
resources.py ALL
|
||||
mccabe-complexity = 12
|
||||
|
Loading…
Reference in New Issue
Block a user