Merge branch 'master' of ssh://lupin/qutebrowser
This commit is contained in:
commit
66fd51958b
113
TODO
113
TODO
@ -14,34 +14,26 @@ Crashes
|
||||
- Chosing printer in :print gives us a segfault on Linux, but :printpreview
|
||||
works...
|
||||
|
||||
- Segfault when closing some tab:
|
||||
QIODevice::read: device not open
|
||||
QIODevice::read: device not open
|
||||
QIODevice::read: device not open
|
||||
Fatal Python error: Segmentation fault
|
||||
- When following a hint:
|
||||
|
||||
Current thread 0x00007ff4ed080700 (most recent call first):
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 29 in main
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 33 in <module>
|
||||
File "/usr/lib/python3.4/runpy.py", line 86 in _run_code
|
||||
File "/usr/lib/python3.4/runpy.py", line 171 in _run_module_as_main
|
||||
QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.
|
||||
|
||||
@ e5000c315dd29ae9356e1b33ed041917c637c85b
|
||||
- When enabling javascript on http://google.com/accounts and reloading:
|
||||
|
||||
- Weird TypeError:
|
||||
OpenType support missing for script 12
|
||||
|
||||
QIODevice::read: device not open
|
||||
QIODevice::read: device not open
|
||||
Traceback (most recent call last):
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 567, in command_handler
|
||||
handler(*args)
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/_tabbedbrowser.py", line 216, in tabclose
|
||||
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/webview.py", line 215, in shutdown
|
||||
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
|
||||
TypeError: pyqtSignal must be bound to a QObject, not 'NetworkManager'
|
||||
Then it quits.
|
||||
When javascript is enabled already, hangs.
|
||||
With minimal browser, prints error but continues to run.
|
||||
|
||||
Also quits on http://de.wikipedia.org/wiki/Hallo-Welt-Programm
|
||||
|
||||
It seems this only happens on Windows.
|
||||
|
||||
- Closing some tabs and undo all them -> quits and CMD.exe hangs
|
||||
|
||||
QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not dequeu request
|
||||
|
||||
@ e5000c315dd29ae9356e1b33ed041917c637c85b
|
||||
|
||||
- Sometimes self._frame is None in on_mode_left in HintManager
|
||||
|
||||
@ -52,6 +44,24 @@ Bugs
|
||||
====
|
||||
|
||||
- All kind of FIXMEs
|
||||
- F on duckduckgo result page opens in current page
|
||||
|
||||
It seems we don't get a linkClicked signal there.
|
||||
Maybe it does some weird js stuff?
|
||||
See also: http://qt-project.org/doc/qt-4.8/qwebpage.html#createWindow
|
||||
Asked here: http://stackoverflow.com/q/23498666/2085149
|
||||
|
||||
- Shutdown is still flaky.
|
||||
|
||||
Some pointers:
|
||||
https://code.google.com/p/webscraping/source/browse/webkit.py
|
||||
Simply does setPage(None) in __del__ of webview.
|
||||
|
||||
http://www.tenox.net/out/wrp11-qt.py
|
||||
does del self._window; del self._view; del self._page
|
||||
|
||||
http://pydoc.net/Python/grab/0.4.5/ghost.ghost/
|
||||
does webview.close(); del self.manager; del self.page; del self.mainframe
|
||||
|
||||
Style
|
||||
=====
|
||||
@ -80,6 +90,9 @@ Major features
|
||||
Minor features
|
||||
==============
|
||||
|
||||
- Enable disk caching
|
||||
QNetworkManager.setCache() and use a QNetworkDiskCache probably
|
||||
- clear cookies
|
||||
- keybind/aliases should have completion for commands/arguments
|
||||
- Hiding scrollbars
|
||||
- Ctrl+A/X to increase/decrease last number in URL
|
||||
@ -102,8 +115,15 @@ hints
|
||||
- filter close hints when it's the same link
|
||||
- ignore keypresses shortly after link following
|
||||
|
||||
Qt Bugs
|
||||
========
|
||||
Useful things
|
||||
=============
|
||||
|
||||
http://www.tenox.net/out/wrp11-qt.py
|
||||
https://code.google.com/p/webscraping/source/browse/webkit.py
|
||||
https://github.com/jeanphix/Ghost.py/blob/master/ghost/ghost.py
|
||||
|
||||
Upstream Bugs
|
||||
=============
|
||||
|
||||
- Printing under windows produced blank pages
|
||||
https://bugreports.qt-project.org/browse/QTBUG-19571
|
||||
@ -116,11 +136,54 @@ Qt Bugs
|
||||
https://bugreports.qt-project.org/browse/QTBUG-38669
|
||||
|
||||
- Web inspector is blank unless .hide()/.show() is called.
|
||||
Asked on SO: http://stackoverflow.com/q/23499159/2085149
|
||||
|
||||
- Weird font rendering
|
||||
https://bugreports.qt-project.org/browse/QTBUG-20973
|
||||
https://bugreports.qt-project.org/browse/QTBUG-21036
|
||||
|
||||
- dead_actute
|
||||
https://bugs.freedesktop.org/show_bug.cgi?id=69476
|
||||
|
||||
- QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.
|
||||
https://bugreports.qt-project.org/browse/QTBUG-30298
|
||||
|
||||
|
||||
Probably fixed crashes
|
||||
======================
|
||||
|
||||
- Segfault when closing some tab:
|
||||
QIODevice::read: device not open
|
||||
QIODevice::read: device not open
|
||||
QIODevice::read: device not open
|
||||
Fatal Python error: Segmentation fault
|
||||
|
||||
Current thread 0x00007ff4ed080700 (most recent call first):
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 29 in main
|
||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 33 in <module>
|
||||
File "/usr/lib/python3.4/runpy.py", line 86 in _run_code
|
||||
File "/usr/lib/python3.4/runpy.py", line 171 in _run_module_as_main
|
||||
|
||||
@ e5000c315dd29ae9356e1b33ed041917c637c85b
|
||||
|
||||
Probably fixed by de7c6a63b48f66e164bf4baa6e892bcbb326d6b9
|
||||
|
||||
- When opening a hint with F:
|
||||
|
||||
2014-05-06 09:01:25 [DEBUG] [_tabbedbrowser:tabopen:153] Opening PyQt5.QtCore.QUrl('http://ddg.gg/')
|
||||
Traceback (most recent call last):
|
||||
File ".\qutebrowser\widgets\_tabbedbrowser.py", line 155, in tabopen
|
||||
tab = WebView(self)
|
||||
File ".\qutebrowser\widgets\webview.py", line 83, in __init__
|
||||
self.page_ = BrowserPage(self)
|
||||
File ".\qutebrowser\browser\webpage.py", line 44, in __init__
|
||||
self.setNetworkAccessManager(QApplication.instance().networkmanager)
|
||||
RuntimeError: wrapped C/C++ object of type NetworkManager has been deleted
|
||||
|
||||
Probably fixed by de7c6a63b48f66e164bf4baa6e892bcbb326d6b9
|
||||
|
||||
|
||||
|
||||
Keybinding stuff (from dwb)
|
||||
===========================
|
||||
|
||||
|
@ -20,9 +20,10 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import functools
|
||||
import subprocess
|
||||
import configparser
|
||||
from bdb import BdbQuit
|
||||
from functools import partial
|
||||
from signal import signal, SIGINT
|
||||
from argparse import ArgumentParser
|
||||
from base64 import b64encode
|
||||
@ -102,7 +103,11 @@ class QuteBrowser(QApplication):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(sys.argv)
|
||||
self._quit_status = {}
|
||||
self._quit_status = {
|
||||
'crash': True,
|
||||
'tabs': False,
|
||||
'networkmanager': False
|
||||
}
|
||||
self._timers = []
|
||||
self._opened_urls = []
|
||||
self._shutting_down = False
|
||||
@ -392,15 +397,22 @@ class QuteBrowser(QApplication):
|
||||
exc = (exctype, excvalue, tb)
|
||||
sys.__excepthook__(*exc)
|
||||
|
||||
if not issubclass(exctype, Exception):
|
||||
# probably a KeyboardInterrupt
|
||||
self._quit_status['crash'] = False
|
||||
|
||||
# Give key input back for crash dialog
|
||||
try:
|
||||
self.removeEventFilter(self.modeman)
|
||||
except AttributeError:
|
||||
# self.modeman could be None
|
||||
pass
|
||||
|
||||
if exctype is BdbQuit or not issubclass(exctype, Exception):
|
||||
# pdb exit, KeyboardInterrupt, ...
|
||||
try:
|
||||
self.shutdown()
|
||||
return
|
||||
except Exception:
|
||||
self.quit()
|
||||
self._quit_status['crash'] = False
|
||||
self._quit_status['shutdown'] = False
|
||||
try:
|
||||
pages = self._recover_pages()
|
||||
except Exception:
|
||||
@ -483,49 +495,55 @@ class QuteBrowser(QApplication):
|
||||
|
||||
@pyqtSlot()
|
||||
@cmdutils.register(instance='', name=['quit', 'q'], nargs=0)
|
||||
def shutdown(self, do_quit=True):
|
||||
def shutdown(self):
|
||||
"""Try to shutdown everything cleanly.
|
||||
|
||||
For some reason lastWindowClosing sometimes seem to get emitted twice,
|
||||
so we make sure we only run once here.
|
||||
|
||||
Args:
|
||||
do_quit: Whether to quit after shutting down.
|
||||
"""
|
||||
if self._shutting_down:
|
||||
return
|
||||
self._shutting_down = True
|
||||
logging.debug("Shutting down... (do_quit={})".format(do_quit))
|
||||
logging.debug("Shutting down...")
|
||||
# Save config
|
||||
if self.config.get('general', 'auto-save-config'):
|
||||
try:
|
||||
self.config.save()
|
||||
except AttributeError:
|
||||
logging.exception("Could not save config.")
|
||||
# Save command history
|
||||
try:
|
||||
self.cmd_history.save()
|
||||
except AttributeError:
|
||||
logging.exception("Could not save command history.")
|
||||
# Save window state
|
||||
try:
|
||||
self._save_geometry()
|
||||
self.stateconfig.save()
|
||||
except AttributeError:
|
||||
logging.exception("Could not save window geometry.")
|
||||
# Save cookies
|
||||
try:
|
||||
self.cookiejar.save()
|
||||
except AttributeError:
|
||||
logging.exception("Could not save cookies.")
|
||||
# Shut down tabs
|
||||
try:
|
||||
if do_quit:
|
||||
self.mainwindow.tabs.shutdown_complete.connect(
|
||||
self.on_tab_shutdown_complete)
|
||||
else:
|
||||
self.mainwindow.tabs.shutdown_complete.connect(
|
||||
functools.partial(self._maybe_quit, 'shutdown'))
|
||||
self.mainwindow.tabs.shutdown_complete.connect(partial(
|
||||
self._maybe_quit, 'tabs'))
|
||||
self.mainwindow.tabs.shutdown()
|
||||
except AttributeError: # mainwindow or tabs could still be None
|
||||
logging.exception("No mainwindow/tabs to shut down.")
|
||||
if do_quit:
|
||||
self.quit()
|
||||
self._maybe_quit('tabs')
|
||||
# Shut down networkmanager
|
||||
try:
|
||||
self.networkmanager.abort_requests()
|
||||
self.networkmanager.destroyed.connect(partial(
|
||||
self._maybe_quit, 'networkmanager'))
|
||||
self.networkmanager.deleteLater()
|
||||
except AttributeError:
|
||||
logging.exception("No networkmanager to shut down.")
|
||||
self._maybe_quit('networkmanager')
|
||||
|
||||
@pyqtSlot()
|
||||
def on_tab_shutdown_complete(self):
|
||||
|
@ -21,7 +21,7 @@ import logging
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QPoint
|
||||
from PyQt5.QtGui import QMouseEvent, QClipboard
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
@ -70,6 +70,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
|
||||
HINT_CSS = """
|
||||
display: {display};
|
||||
color: {config[colors][hints.fg]};
|
||||
background: {config[colors][hints.bg]};
|
||||
font: {config[fonts][hints]};
|
||||
@ -193,6 +194,24 @@ class HintManager(QObject):
|
||||
hintstr.insert(0, chars[0])
|
||||
return ''.join(hintstr)
|
||||
|
||||
def _get_hint_css(self, elem, label=None):
|
||||
"""Get the hint CSS for the element given.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to get the CSS for.
|
||||
label: The label QWebElement if display: none should be preserved.
|
||||
|
||||
Return:
|
||||
The CSS to set as a string.
|
||||
"""
|
||||
if label is None or label.attribute('hidden') != 'true':
|
||||
display = 'inline'
|
||||
else:
|
||||
display = 'none'
|
||||
rect = elem.geometry()
|
||||
return self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
||||
config=config.instance(), display=display)
|
||||
|
||||
def _draw_label(self, elem, string):
|
||||
"""Draw a hint label over an element.
|
||||
|
||||
@ -203,9 +222,7 @@ class HintManager(QObject):
|
||||
Return:
|
||||
The newly created label elment
|
||||
"""
|
||||
rect = elem.geometry()
|
||||
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
||||
config=config.instance())
|
||||
css = self._get_hint_css(elem)
|
||||
doc = self._frame.documentElement()
|
||||
# It seems impossible to create an empty QWebElement for which isNull()
|
||||
# is false so we can work with it.
|
||||
@ -227,7 +244,13 @@ class HintManager(QObject):
|
||||
else:
|
||||
target = self._target
|
||||
self.set_open_target.emit(Target[target])
|
||||
point = elem.geometry().topLeft()
|
||||
# FIXME this is a quick & dirty fix, we should:
|
||||
# a) Have better heuristics where to click at (e.g. end of input
|
||||
# fields)
|
||||
# b) Check border/margin/padding to know where to click
|
||||
# Hinting failed here for example:
|
||||
# https://lsf.fh-worms.de/
|
||||
point = elem.geometry().topLeft() + QPoint(1, 1)
|
||||
scrollpos = self._frame.scrollPosition()
|
||||
logging.debug("Clicking on \"{}\" at {}/{} - {}/{}".format(
|
||||
elem.toPlainText(), point.x(), point.y(), scrollpos.x(),
|
||||
@ -382,34 +405,48 @@ class HintManager(QObject):
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
delete = []
|
||||
logging.debug("Handling new keystring: '{}'".format(keystr))
|
||||
for (string, elems) in self._elems.items():
|
||||
if string.startswith(keystr):
|
||||
matched = string[:len(keystr)]
|
||||
rest = string[len(keystr):]
|
||||
elems.label.setInnerXml('<font color="{}">{}</font>{}'.format(
|
||||
config.get('colors', 'hints.fg.match'), matched, rest))
|
||||
if elems.label.attribute('hidden') == 'true':
|
||||
# hidden element which matches again -> unhide it
|
||||
elems.label.setAttribute('hidden', 'false')
|
||||
css = self._get_hint_css(elems.elem, elems.label)
|
||||
elems.label.setAttribute('style', css)
|
||||
else:
|
||||
elems.label.removeFromDocument()
|
||||
delete.append(string)
|
||||
for key in delete:
|
||||
del self._elems[key]
|
||||
# element doesn't match anymore -> hide it
|
||||
elems.label.setAttribute('hidden', 'true')
|
||||
css = self._get_hint_css(elems.elem, elems.label)
|
||||
elems.label.setAttribute('style', css)
|
||||
|
||||
def filter_hints(self, filterstr):
|
||||
"""Filter displayed hints according to a text."""
|
||||
delete = []
|
||||
for (string, elems) in self._elems.items():
|
||||
if not elems.elem.toPlainText().lower().startswith(filterstr):
|
||||
elems.label.removeFromDocument()
|
||||
delete.append(string)
|
||||
for key in delete:
|
||||
del self._elems[key]
|
||||
if not self._elems:
|
||||
for elems in self._elems.values():
|
||||
if elems.elem.toPlainText().lower().startswith(filterstr):
|
||||
if elems.label.attribute('hidden') == 'true':
|
||||
# hidden element which matches again -> unhide it
|
||||
elems.label.setAttribute('hidden', 'false')
|
||||
css = self._get_hint_css(elems.elem, elems.label)
|
||||
elems.label.setAttribute('style', css)
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
elems.label.setAttribute('hidden', 'true')
|
||||
css = self._get_hint_css(elems.elem, elems.label)
|
||||
elems.label.setAttribute('style', css)
|
||||
visible = {}
|
||||
for k, e in self._elems.items():
|
||||
if e.label.attribute('hidden') != 'true':
|
||||
visible[k] = e
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave('hint')
|
||||
elif len(self._elems) == 1 and config.get('hints', 'auto-follow'):
|
||||
elif len(visible) == 1 and config.get('hints', 'auto-follow'):
|
||||
# unpacking gets us the first (and only) key in the dict.
|
||||
self.fire(*self._elems)
|
||||
self.fire(*visible)
|
||||
|
||||
def fire(self, keystr, force=False):
|
||||
"""Fire a completed hint.
|
||||
@ -461,9 +498,7 @@ class HintManager(QObject):
|
||||
def on_contents_size_changed(self, _size):
|
||||
"""Reposition hints if contents size changed."""
|
||||
for elems in self._elems.values():
|
||||
rect = elems.elem.geometry()
|
||||
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
||||
config=config.instance())
|
||||
css = self._get_hint_css(elems.elem, elems.label)
|
||||
elems.label.setAttribute('style', css)
|
||||
|
||||
@pyqtSlot(str)
|
||||
@ -472,9 +507,14 @@ class HintManager(QObject):
|
||||
if mode != 'hint':
|
||||
return
|
||||
for elem in self._elems.values():
|
||||
elem.label.removeFromDocument()
|
||||
self._frame.contentsSizeChanged.disconnect(
|
||||
self.on_contents_size_changed)
|
||||
if not elem.label.isNull():
|
||||
elem.label.removeFromDocument()
|
||||
if self._frame is not None:
|
||||
# The frame which was focused in start() might not be available
|
||||
# anymore, since Qt might already have deleted it (e.g. when a new
|
||||
# page is loaded).
|
||||
self._frame.contentsSizeChanged.disconnect(
|
||||
self.on_contents_size_changed)
|
||||
self._elems = {}
|
||||
self._to_follow = None
|
||||
self._target = None
|
||||
|
@ -72,7 +72,7 @@ class BrowserPage(QWebPage):
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
ua = config.get('network', 'user-agent')
|
||||
if not ua:
|
||||
if ua is None:
|
||||
return super().userAgentForUrl(url)
|
||||
else:
|
||||
return ua
|
||||
|
@ -155,17 +155,22 @@ class String(BaseType):
|
||||
Attributes:
|
||||
minlen: Minimum length (inclusive).
|
||||
maxlen: Maximum length (inclusive).
|
||||
forbidden: Forbidden chars in the string.
|
||||
none: Whether to convert to None for an empty string.
|
||||
"""
|
||||
|
||||
typestr = 'string'
|
||||
|
||||
def __init__(self, minlen=None, maxlen=None, forbidden=None):
|
||||
def __init__(self, minlen=None, maxlen=None, forbidden=None, none=False):
|
||||
self.minlen = minlen
|
||||
self.maxlen = maxlen
|
||||
self.forbidden = forbidden
|
||||
self.none = none
|
||||
|
||||
def transform(self, value):
|
||||
return value.lower()
|
||||
if self.none and not value:
|
||||
return None
|
||||
return value
|
||||
|
||||
def validate(self, value):
|
||||
if self.forbidden is not None and any(c in value
|
||||
@ -180,37 +185,17 @@ class String(BaseType):
|
||||
self.maxlen))
|
||||
|
||||
|
||||
class ShellCommand(String):
|
||||
class List(BaseType):
|
||||
|
||||
"""A shellcommand which is split via shlex.
|
||||
"""Base class for a (string-)list setting."""
|
||||
|
||||
Attributes:
|
||||
placeholder: If there should be a placeholder.
|
||||
"""
|
||||
|
||||
typestr = 'shell-cmd'
|
||||
|
||||
def __init__(self, placeholder=False):
|
||||
self.placeholder = placeholder
|
||||
super().__init__()
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
if self.placeholder and '{}' not in value:
|
||||
raise ValidationError(value, "needs to contain a {}-placeholder.")
|
||||
typestr = 'string-list'
|
||||
|
||||
def transform(self, value):
|
||||
return shlex.split(value)
|
||||
return value.split(',')
|
||||
|
||||
|
||||
class HintMode(BaseType):
|
||||
|
||||
"""Base class for the hints -> mode setting."""
|
||||
|
||||
typestr = 'hint-mode'
|
||||
|
||||
valid_values = ValidValues(('number', "Use numeric hints."),
|
||||
('letter', "Use the chars in hints -> chars."))
|
||||
def validate(self, value):
|
||||
pass
|
||||
|
||||
|
||||
class Bool(BaseType):
|
||||
@ -245,18 +230,24 @@ class Int(BaseType):
|
||||
Attributes:
|
||||
minval: Minimum value (inclusive).
|
||||
maxval: Maximum value (inclusive).
|
||||
none: Whether to accept empty values as None.
|
||||
"""
|
||||
|
||||
typestr = 'int'
|
||||
|
||||
def __init__(self, minval=None, maxval=None):
|
||||
def __init__(self, minval=None, maxval=None, none=False):
|
||||
self.minval = minval
|
||||
self.maxval = maxval
|
||||
self.none = none
|
||||
|
||||
def transform(self, value):
|
||||
if self.none and not value:
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
def validate(self, value):
|
||||
if self.none and not value:
|
||||
return
|
||||
try:
|
||||
intval = int(value)
|
||||
except ValueError:
|
||||
@ -269,20 +260,21 @@ class Int(BaseType):
|
||||
self.maxval))
|
||||
|
||||
|
||||
class NoneInt(BaseType):
|
||||
class IntList(List):
|
||||
|
||||
"""Int or None."""
|
||||
"""Base class for an int-list setting."""
|
||||
|
||||
typestr = 'int-list'
|
||||
|
||||
def transform(self, value):
|
||||
if value == '':
|
||||
return None
|
||||
else:
|
||||
return super().transform(value)
|
||||
vals = super().transform(value)
|
||||
return map(int, vals)
|
||||
|
||||
def validate(self, value):
|
||||
if value == '':
|
||||
return
|
||||
super().validate(value)
|
||||
try:
|
||||
self.transform(value)
|
||||
except ValueError:
|
||||
raise ValidationError(value, "must be a list of integers!")
|
||||
|
||||
|
||||
class Float(BaseType):
|
||||
@ -316,36 +308,6 @@ class Float(BaseType):
|
||||
self.maxval))
|
||||
|
||||
|
||||
class List(BaseType):
|
||||
|
||||
"""Base class for a (string-)list setting."""
|
||||
|
||||
typestr = 'string-list'
|
||||
|
||||
def transform(self, value):
|
||||
return value.split(',')
|
||||
|
||||
def validate(self, value):
|
||||
pass
|
||||
|
||||
|
||||
class IntList(List):
|
||||
|
||||
"""Base class for an int-list setting."""
|
||||
|
||||
typestr = 'int-list'
|
||||
|
||||
def transform(self, value):
|
||||
vals = super().transform(value)
|
||||
return map(int, vals)
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
self.transform(value)
|
||||
except ValueError:
|
||||
raise ValidationError(value, "must be a list of integers!")
|
||||
|
||||
|
||||
class Perc(BaseType):
|
||||
|
||||
"""Percentage.
|
||||
@ -355,6 +317,8 @@ class Perc(BaseType):
|
||||
maxval: Maximum value (inclusive).
|
||||
"""
|
||||
|
||||
typestr = 'percentage'
|
||||
|
||||
def __init__(self, minval=None, maxval=None):
|
||||
self.minval = minval
|
||||
self.maxval = maxval
|
||||
@ -406,15 +370,6 @@ class PercList(List):
|
||||
raise ValidationError(value, "must be a list of percentages!")
|
||||
|
||||
|
||||
class ZoomPerc(Perc):
|
||||
|
||||
"""A percentage which needs to be in the current zoom percentages."""
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
# FIXME we should validate the percentage is in the list here.
|
||||
|
||||
|
||||
class PercOrInt(BaseType):
|
||||
|
||||
"""Percentage or integer.
|
||||
@ -426,6 +381,8 @@ class PercOrInt(BaseType):
|
||||
maxint: Maximum value for integer (inclusive).
|
||||
"""
|
||||
|
||||
typestr = 'percentage-or-int'
|
||||
|
||||
def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None):
|
||||
self.minperc = minperc
|
||||
self.maxperc = maxperc
|
||||
@ -457,130 +414,6 @@ class PercOrInt(BaseType):
|
||||
self.maxint))
|
||||
|
||||
|
||||
class WebKitBytes(BaseType):
|
||||
|
||||
"""A size with an optional suffix.
|
||||
|
||||
Attributes:
|
||||
maxsize: The maximum size to be used.
|
||||
|
||||
Class attributes:
|
||||
SUFFIXES: A mapping of size suffixes to multiplicators.
|
||||
"""
|
||||
|
||||
SUFFIXES = {
|
||||
'k': 1024 ** 1,
|
||||
'm': 1024 ** 2,
|
||||
'g': 1024 ** 3,
|
||||
't': 1024 ** 4,
|
||||
'p': 1024 ** 5,
|
||||
'e': 1024 ** 6,
|
||||
'z': 1024 ** 7,
|
||||
'y': 1024 ** 8,
|
||||
}
|
||||
|
||||
def __init__(self, maxsize=None):
|
||||
self.maxsize = maxsize
|
||||
|
||||
def validate(self, value):
|
||||
if value == '':
|
||||
return
|
||||
try:
|
||||
val = self.transform(value)
|
||||
except ValueError:
|
||||
raise ValidationError(value, "must be a valid integer with "
|
||||
"optional suffix!")
|
||||
if self.maxsize is not None and val > self.maxsize:
|
||||
raise ValidationError(value, "must be {} "
|
||||
"maximum!".format(self.maxsize))
|
||||
|
||||
def transform(self, value):
|
||||
if value == '':
|
||||
return None
|
||||
if any(value.lower().endswith(c) for c in self.SUFFIXES):
|
||||
suffix = value[-1].lower()
|
||||
val = value[:-1]
|
||||
multiplicator = self.SUFFIXES[suffix]
|
||||
else:
|
||||
val = value
|
||||
multiplicator = 1
|
||||
return int(val) * multiplicator
|
||||
|
||||
|
||||
class WebKitBytesList(List):
|
||||
|
||||
"""A size with an optional suffix.
|
||||
|
||||
Attributes:
|
||||
length: The length of the list.
|
||||
bytestype: The webkit bytes type.
|
||||
"""
|
||||
|
||||
def __init__(self, maxsize=None, length=None):
|
||||
self.length = length
|
||||
self.bytestype = WebKitBytes(maxsize)
|
||||
|
||||
def transform(self, value):
|
||||
if value == '':
|
||||
return None
|
||||
vals = super().transform(value)
|
||||
return [self.bytestype.transform(val) for val in vals]
|
||||
|
||||
def validate(self, value):
|
||||
if value == '':
|
||||
return
|
||||
vals = super().transform(value)
|
||||
for val in vals:
|
||||
self.bytestype.validate(val)
|
||||
if any(val is None for val in vals):
|
||||
raise ValidationError(value, "all values need to be set!")
|
||||
if self.length is not None and len(vals) != self.length:
|
||||
raise ValidationError(value, "exactly {} values need to be "
|
||||
"set!".format(self.length))
|
||||
|
||||
|
||||
class Proxy(BaseType):
|
||||
|
||||
"""A proxy URL or special value."""
|
||||
|
||||
valid_values = ValidValues(('system', "Use the system wide proxy."),
|
||||
('none', "Don't use any proxy"))
|
||||
|
||||
PROXY_TYPES = {
|
||||
'http': QNetworkProxy.HttpProxy,
|
||||
'socks': QNetworkProxy.Socks5Proxy,
|
||||
'socks5': QNetworkProxy.Socks5Proxy,
|
||||
}
|
||||
|
||||
def validate(self, value):
|
||||
if value in self.valid_values:
|
||||
return
|
||||
url = QUrl(value)
|
||||
if (url.isValid() and not url.isEmpty() and
|
||||
url.scheme() in self.PROXY_TYPES):
|
||||
return
|
||||
raise ValidationError(value, "must be a proxy URL (http://... or "
|
||||
"socks://...) or system/none!")
|
||||
|
||||
def complete(self):
|
||||
out = []
|
||||
for val in self.valid_values:
|
||||
out.append((val, self.valid_values.descriptions[val]))
|
||||
out.append(('http://', 'HTTP proxy URL'))
|
||||
out.append(('socks://', 'SOCKS proxy URL'))
|
||||
return out
|
||||
|
||||
def transform(self, value):
|
||||
if value == 'system':
|
||||
return None
|
||||
elif value == 'none':
|
||||
return QNetworkProxy(QNetworkProxy.NoProxy)
|
||||
url = QUrl(value)
|
||||
typ = self.PROXY_TYPES[url.scheme()]
|
||||
return QNetworkProxy(typ, url.host(), url.port(), url.userName(),
|
||||
url.password())
|
||||
|
||||
|
||||
class Command(BaseType):
|
||||
|
||||
"""Base class for a command value with arguments."""
|
||||
@ -646,6 +479,225 @@ class Font(BaseType):
|
||||
pass
|
||||
|
||||
|
||||
class Regex(BaseType):
|
||||
|
||||
"""A regular expression."""
|
||||
|
||||
typestr = 'regex'
|
||||
|
||||
def __init__(self, flags=0):
|
||||
self.flags = flags
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
re.compile(value, self.flags)
|
||||
except RegexError as e:
|
||||
raise ValidationError(value, "must be a valid regex - " + str(e))
|
||||
|
||||
def transform(self, value):
|
||||
return re.compile(value, self.flags)
|
||||
|
||||
|
||||
class RegexList(List):
|
||||
|
||||
"""A list of regexes."""
|
||||
|
||||
typestr = 'regex-list'
|
||||
|
||||
def __init__(self, flags=0):
|
||||
self.flags = flags
|
||||
|
||||
def transform(self, value):
|
||||
vals = super().transform(value)
|
||||
return [re.compile(pattern, self.flags) for pattern in vals]
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
self.transform(value)
|
||||
except RegexError as e:
|
||||
raise ValidationError(value, "must be a list valid regexes - " +
|
||||
str(e))
|
||||
|
||||
|
||||
class File(BaseType):
|
||||
|
||||
"""A file on the local filesystem."""
|
||||
|
||||
typestr = 'file'
|
||||
|
||||
def validate(self, value):
|
||||
if not os.path.isfile(value):
|
||||
raise ValidationError(value, "must be a valid file!")
|
||||
|
||||
|
||||
class WebKitBytes(BaseType):
|
||||
|
||||
"""A size with an optional suffix.
|
||||
|
||||
Attributes:
|
||||
maxsize: The maximum size to be used.
|
||||
|
||||
Class attributes:
|
||||
SUFFIXES: A mapping of size suffixes to multiplicators.
|
||||
"""
|
||||
|
||||
SUFFIXES = {
|
||||
'k': 1024 ** 1,
|
||||
'm': 1024 ** 2,
|
||||
'g': 1024 ** 3,
|
||||
't': 1024 ** 4,
|
||||
'p': 1024 ** 5,
|
||||
'e': 1024 ** 6,
|
||||
'z': 1024 ** 7,
|
||||
'y': 1024 ** 8,
|
||||
}
|
||||
|
||||
typestr = 'bytes'
|
||||
|
||||
def __init__(self, maxsize=None):
|
||||
self.maxsize = maxsize
|
||||
|
||||
def validate(self, value):
|
||||
if value == '':
|
||||
return
|
||||
try:
|
||||
val = self.transform(value)
|
||||
except ValueError:
|
||||
raise ValidationError(value, "must be a valid integer with "
|
||||
"optional suffix!")
|
||||
if self.maxsize is not None and val > self.maxsize:
|
||||
raise ValidationError(value, "must be {} "
|
||||
"maximum!".format(self.maxsize))
|
||||
|
||||
def transform(self, value):
|
||||
if value == '':
|
||||
return None
|
||||
if any(value.lower().endswith(c) for c in self.SUFFIXES):
|
||||
suffix = value[-1].lower()
|
||||
val = value[:-1]
|
||||
multiplicator = self.SUFFIXES[suffix]
|
||||
else:
|
||||
val = value
|
||||
multiplicator = 1
|
||||
return int(val) * multiplicator
|
||||
|
||||
|
||||
class WebKitBytesList(List):
|
||||
|
||||
"""A size with an optional suffix.
|
||||
|
||||
Attributes:
|
||||
length: The length of the list.
|
||||
bytestype: The webkit bytes type.
|
||||
"""
|
||||
|
||||
typestr = 'bytes-list'
|
||||
|
||||
def __init__(self, maxsize=None, length=None):
|
||||
self.length = length
|
||||
self.bytestype = WebKitBytes(maxsize)
|
||||
|
||||
def transform(self, value):
|
||||
if value == '':
|
||||
return None
|
||||
vals = super().transform(value)
|
||||
return [self.bytestype.transform(val) for val in vals]
|
||||
|
||||
def validate(self, value):
|
||||
if value == '':
|
||||
return
|
||||
vals = super().transform(value)
|
||||
for val in vals:
|
||||
self.bytestype.validate(val)
|
||||
if any(val is None for val in vals):
|
||||
raise ValidationError(value, "all values need to be set!")
|
||||
if self.length is not None and len(vals) != self.length:
|
||||
raise ValidationError(value, "exactly {} values need to be "
|
||||
"set!".format(self.length))
|
||||
|
||||
|
||||
class ShellCommand(String):
|
||||
|
||||
"""A shellcommand which is split via shlex.
|
||||
|
||||
Attributes:
|
||||
placeholder: If there should be a placeholder.
|
||||
"""
|
||||
|
||||
typestr = 'shell-command'
|
||||
|
||||
def __init__(self, placeholder=False):
|
||||
self.placeholder = placeholder
|
||||
super().__init__()
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
if self.placeholder and '{}' not in value:
|
||||
raise ValidationError(value, "needs to contain a {}-placeholder.")
|
||||
|
||||
def transform(self, value):
|
||||
return shlex.split(value)
|
||||
|
||||
|
||||
class ZoomPerc(Perc):
|
||||
|
||||
"""A percentage which needs to be in the current zoom percentages."""
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
# FIXME we should validate the percentage is in the list here.
|
||||
|
||||
|
||||
class HintMode(BaseType):
|
||||
|
||||
"""Base class for the hints -> mode setting."""
|
||||
|
||||
valid_values = ValidValues(('number', "Use numeric hints."),
|
||||
('letter', "Use the chars in hints -> chars."))
|
||||
|
||||
|
||||
class Proxy(BaseType):
|
||||
|
||||
"""A proxy URL or special value."""
|
||||
|
||||
valid_values = ValidValues(('system', "Use the system wide proxy."),
|
||||
('none', "Don't use any proxy"))
|
||||
|
||||
PROXY_TYPES = {
|
||||
'http': QNetworkProxy.HttpProxy,
|
||||
'socks': QNetworkProxy.Socks5Proxy,
|
||||
'socks5': QNetworkProxy.Socks5Proxy,
|
||||
}
|
||||
|
||||
def validate(self, value):
|
||||
if value in self.valid_values:
|
||||
return
|
||||
url = QUrl(value)
|
||||
if (url.isValid() and not url.isEmpty() and
|
||||
url.scheme() in self.PROXY_TYPES):
|
||||
return
|
||||
raise ValidationError(value, "must be a proxy URL (http://... or "
|
||||
"socks://...) or system/none!")
|
||||
|
||||
def complete(self):
|
||||
out = []
|
||||
for val in self.valid_values:
|
||||
out.append((val, self.valid_values.descriptions[val]))
|
||||
out.append(('http://', 'HTTP proxy URL'))
|
||||
out.append(('socks://', 'SOCKS proxy URL'))
|
||||
return out
|
||||
|
||||
def transform(self, value):
|
||||
if value == 'system':
|
||||
return None
|
||||
elif value == 'none':
|
||||
return QNetworkProxy(QNetworkProxy.NoProxy)
|
||||
url = QUrl(value)
|
||||
typ = self.PROXY_TYPES[url.scheme()]
|
||||
return QNetworkProxy(typ, url.host(), url.port(), url.userName(),
|
||||
url.password())
|
||||
|
||||
|
||||
class SearchEngineName(BaseType):
|
||||
|
||||
"""A search engine name."""
|
||||
@ -673,55 +725,19 @@ class KeyBindingName(BaseType):
|
||||
pass
|
||||
|
||||
|
||||
class Regex(BaseType):
|
||||
class KeyBinding(Command):
|
||||
|
||||
"""A regular expression."""
|
||||
"""The command of a keybinding."""
|
||||
|
||||
def __init__(self, flags=0):
|
||||
self.flags = flags
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
re.compile(value, self.flags)
|
||||
except RegexError as e:
|
||||
raise ValidationError(value, "must be a valid regex - " + str(e))
|
||||
|
||||
def transform(self, value):
|
||||
return re.compile(value, self.flags)
|
||||
|
||||
|
||||
class RegexList(List):
|
||||
|
||||
"""A list of regexes."""
|
||||
|
||||
def __init__(self, flags=0):
|
||||
self.flags = flags
|
||||
|
||||
def transform(self, value):
|
||||
vals = super().transform(value)
|
||||
return [re.compile(pattern, self.flags) for pattern in vals]
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
self.transform(value)
|
||||
except RegexError as e:
|
||||
raise ValidationError(value, "must be a list valid regexes - " +
|
||||
str(e))
|
||||
|
||||
|
||||
class File(BaseType):
|
||||
|
||||
"""A file on the local filesystem."""
|
||||
|
||||
def validate(self, value):
|
||||
if not os.path.isfile(value):
|
||||
raise ValidationError(value, "must be a valid file!")
|
||||
pass
|
||||
|
||||
|
||||
class WebSettingsFile(File):
|
||||
|
||||
"""QWebSettings file which also can be none."""
|
||||
|
||||
typestr = 'file'
|
||||
|
||||
def validate(self, value):
|
||||
if value == '':
|
||||
# empty values are okay
|
||||
@ -791,10 +807,3 @@ class AcceptCookies(String):
|
||||
|
||||
valid_values = ValidValues(('default', "Default QtWebKit behaviour"),
|
||||
('never', "Don't accept cookies at all."))
|
||||
|
||||
|
||||
class KeyBinding(Command):
|
||||
|
||||
"""The command of a keybinding."""
|
||||
|
||||
pass
|
||||
|
@ -271,7 +271,6 @@ class ConfigManager(QObject):
|
||||
Return:
|
||||
The value of the option.
|
||||
"""
|
||||
logging.debug("getting {} -> {}".format(sectname, optname))
|
||||
try:
|
||||
sect = self.sections[sectname]
|
||||
except KeyError:
|
||||
@ -285,7 +284,6 @@ class ConfigManager(QObject):
|
||||
mapping = {key: val.value for key, val in sect.values.items()}
|
||||
newval = self._interpolation.before_get(self, sectname, optname,
|
||||
val.value, mapping)
|
||||
logging.debug("interpolated val: {}".format(newval))
|
||||
if transformed:
|
||||
newval = val.typ.transform(newval)
|
||||
return newval
|
||||
|
@ -203,11 +203,11 @@ DATA = OrderedDict([
|
||||
"Value to send in the DNT header."),
|
||||
|
||||
('accept-language',
|
||||
SettingValue(types.String(), 'en-US,en'),
|
||||
SettingValue(types.String(none=True), 'en-US,en'),
|
||||
"Value to send in the accept-language header."),
|
||||
|
||||
('user-agent',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"User agent to send. Empty to send the default."),
|
||||
|
||||
('accept-cookies',
|
||||
@ -422,55 +422,55 @@ DATA = OrderedDict([
|
||||
"User stylesheet to set."),
|
||||
|
||||
('css-media-type',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Set the CSS media type."),
|
||||
|
||||
('default-encoding',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Default encoding to use for websites."),
|
||||
|
||||
('font-family-standard',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Font family for standard fonts."),
|
||||
|
||||
('font-family-fixed',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Font family for fixed fonts."),
|
||||
|
||||
('font-family-serif',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Font family for serif fonts."),
|
||||
|
||||
('font-family-sans-serif',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Font family for sans-serif fonts."),
|
||||
|
||||
('font-family-cursive',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Font family for cursive fonts."),
|
||||
|
||||
('font-family-fantasy',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"Font family for fantasy fonts."),
|
||||
|
||||
('font-size-minimum',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"The hard minimum font size."),
|
||||
|
||||
('font-size-minimum-logical',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"The minimum logical font size that is applied when zooming out."),
|
||||
|
||||
('font-size-default',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"The default font size for regular text."),
|
||||
|
||||
('font-size-default-fixed',
|
||||
SettingValue(types.String(), ''),
|
||||
SettingValue(types.String(none=True), ''),
|
||||
"The default font size for fixed-pitch text."),
|
||||
|
||||
('maximum-pages-in-cache',
|
||||
SettingValue(types.NoneInt(), ''),
|
||||
SettingValue(types.Int(none=True), ''),
|
||||
"The default font size for fixed-pitch text."),
|
||||
|
||||
('object-cache-capacities',
|
||||
@ -485,7 +485,7 @@ DATA = OrderedDict([
|
||||
|
||||
('offline-web-application-cache-quota',
|
||||
SettingValue(types.WebKitBytes(maxsize=INT64_MAX), ''),
|
||||
"Quota for the offline web application cache>"),
|
||||
"Quota for the offline web application cache."),
|
||||
)),
|
||||
|
||||
('hints', sect.KeyValue(
|
||||
|
@ -150,7 +150,8 @@ class BaseKeyParser(QObject):
|
||||
"""
|
||||
logging.debug("Got key: {} / text: '{}'".format(e.key(), e.text()))
|
||||
txt = e.text().strip()
|
||||
if not txt:
|
||||
if not txt or e.key() == Qt.Key_Backspace:
|
||||
# backspace is counted as text...
|
||||
logging.debug("Ignoring, no text")
|
||||
return False
|
||||
|
||||
@ -172,9 +173,13 @@ class BaseKeyParser(QObject):
|
||||
(match, binding) = self._match_key(cmd_input)
|
||||
|
||||
if match == self.Match.definitive:
|
||||
logging.debug("Definitive match for "
|
||||
"\"{}\".".format(self._keystring))
|
||||
self._keystring = ''
|
||||
self.execute(binding, self.Type.chain, count)
|
||||
elif match == self.Match.ambiguous:
|
||||
logging.debug("Ambigious match for "
|
||||
"\"{}\".".format(self._keystring))
|
||||
self._handle_ambiguous_match(binding, count)
|
||||
elif match == self.Match.partial:
|
||||
logging.debug("No match for \"{}\" (added {})".format(
|
||||
|
@ -28,9 +28,11 @@ from PyQt5.QtCore import pyqtSignal, Qt
|
||||
import qutebrowser.utils.message as message
|
||||
import qutebrowser.config.config as config
|
||||
from qutebrowser.keyinput.keyparser import CommandKeyParser
|
||||
from qutebrowser.utils.usertypes import enum
|
||||
|
||||
|
||||
STARTCHARS = ":/?"
|
||||
LastPress = enum('none', 'filtertext', 'keystring')
|
||||
|
||||
|
||||
class NormalKeyParser(CommandKeyParser):
|
||||
@ -69,6 +71,7 @@ class HintKeyParser(CommandKeyParser):
|
||||
|
||||
Attributes:
|
||||
_filtertext: The text to filter with.
|
||||
_last_press: The nature of the last keypress, a LastPress member.
|
||||
"""
|
||||
|
||||
fire_hint = pyqtSignal(str)
|
||||
@ -77,6 +80,7 @@ class HintKeyParser(CommandKeyParser):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent, supports_count=False, supports_chains=True)
|
||||
self._filtertext = ''
|
||||
self._last_press = LastPress.none
|
||||
self.read_config('keybind.hint')
|
||||
|
||||
def _handle_special_key(self, e):
|
||||
@ -92,20 +96,32 @@ class HintKeyParser(CommandKeyParser):
|
||||
|
||||
Emit:
|
||||
filter_hints: Emitted when filter string has changed.
|
||||
keystring_updated: Emitted when keystring has been changed.
|
||||
"""
|
||||
logging.debug("Got special key {} text {}".format(e.key(), e.text()))
|
||||
if config.get('hints', 'mode') != 'number':
|
||||
return super()._handle_special_key(e)
|
||||
elif e.key() == Qt.Key_Backspace:
|
||||
if self._filtertext:
|
||||
if e.key() == Qt.Key_Backspace:
|
||||
logging.debug("Got backspace, mode {}, filtertext \"{}\", "
|
||||
"keystring \"{}\"".format(
|
||||
LastPress[self._last_press], self._filtertext,
|
||||
self._keystring))
|
||||
if self._last_press == LastPress.filtertext and self._filtertext:
|
||||
self._filtertext = self._filtertext[:-1]
|
||||
self.filter_hints.emit(self._filtertext)
|
||||
return True
|
||||
return True
|
||||
elif self._last_press == LastPress.keystring and self._keystring:
|
||||
self._keystring = self._keystring[:-1]
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
return True
|
||||
else:
|
||||
return super()._handle_special_key(e)
|
||||
elif config.get('hints', 'mode') != 'number':
|
||||
return super()._handle_special_key(e)
|
||||
elif not e.text():
|
||||
return super()._handle_special_key(e)
|
||||
else:
|
||||
self._filtertext += e.text()
|
||||
self.filter_hints.emit(self._filtertext)
|
||||
self._last_press = LastPress.filtertext
|
||||
return True
|
||||
|
||||
def handle(self, e):
|
||||
@ -120,6 +136,7 @@ class HintKeyParser(CommandKeyParser):
|
||||
handled = self._handle_single_key(e)
|
||||
if handled:
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self._last_press = LastPress.keystring
|
||||
return handled
|
||||
return self._handle_special_key(e)
|
||||
|
||||
|
@ -72,8 +72,10 @@ class NetworkManager(QNetworkAccessManager):
|
||||
dnt = '0'.encode('ascii')
|
||||
req.setRawHeader('DNT'.encode('ascii'), dnt)
|
||||
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
|
||||
req.setRawHeader('Accept-Language'.encode('ascii'),
|
||||
config.get('network', 'accept-language'))
|
||||
accept_language = config.get('network', 'accept-language')
|
||||
if accept_language is not None:
|
||||
req.setRawHeader('Accept-Language'.encode('ascii'),
|
||||
accept_language.encode('ascii'))
|
||||
reply = super().createRequest(op, req, outgoing_data)
|
||||
self._requests[id(reply)] = reply
|
||||
reply.destroyed.connect(lambda obj: self._requests.pop(id(obj)))
|
||||
|
@ -137,6 +137,7 @@ class IsUrlNaiveTests(TestCase):
|
||||
'foo bar',
|
||||
'localhost test',
|
||||
'another . test',
|
||||
'foo',
|
||||
]
|
||||
|
||||
def test_urls(self):
|
||||
|
@ -20,6 +20,8 @@
|
||||
import re
|
||||
import sys
|
||||
import shlex
|
||||
import urllib.request
|
||||
from urllib.parse import urljoin, urlencode
|
||||
from functools import reduce
|
||||
from pkg_resources import resource_string
|
||||
|
||||
@ -110,3 +112,24 @@ def shell_escape(s):
|
||||
# use single quotes, and put single quotes into double quotes
|
||||
# the string $'b is then quoted as '$'"'"'b'
|
||||
return "'" + s.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
|
||||
def pastebin(text):
|
||||
"""Paste the text into a pastebin and return the URL."""
|
||||
api_url = 'http://paste.the-compiler.org/api/'
|
||||
data = {
|
||||
'text': text,
|
||||
'title': "qutebrowser crash",
|
||||
'name': "qutebrowser",
|
||||
}
|
||||
encoded_data = urlencode(data).encode('utf-8')
|
||||
create_url = urljoin(api_url, 'create')
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
|
||||
}
|
||||
request = urllib.request.Request(create_url, encoded_data, headers)
|
||||
response = urllib.request.urlopen(request)
|
||||
url = response.read().decode('utf-8').rstrip()
|
||||
if not url.startswith('http'):
|
||||
raise ValueError("Got unexpected response: {}".format(url))
|
||||
return url
|
||||
|
@ -73,11 +73,14 @@ def _is_url_naive(url):
|
||||
True if the url really is an URL, False otherwise.
|
||||
"""
|
||||
protocols = ['http', 'https']
|
||||
u = qurl(url)
|
||||
urlstr = urlstring(url)
|
||||
if isinstance(url, QUrl):
|
||||
u = url
|
||||
else:
|
||||
u = QUrl.fromUserInput(url)
|
||||
if u.scheme() in protocols:
|
||||
# We don't use u here because fromUserInput appends http:// automatically.
|
||||
if any(urlstr.startswith(proto) for proto in protocols):
|
||||
return True
|
||||
elif '.' in u.host():
|
||||
return True
|
||||
|
@ -139,7 +139,8 @@ class TabbedBrowser(TabWidget):
|
||||
tab.open_tab.connect(self.tabopen)
|
||||
tab.iconChanged.connect(self.on_icon_changed)
|
||||
|
||||
def _tabopen(self, url, background=False):
|
||||
@pyqtSlot(str, bool)
|
||||
def tabopen(self, url, background=False):
|
||||
"""Open a new tab with a given url.
|
||||
|
||||
Inner logic for tabopen and backtabopen.
|
||||
@ -217,7 +218,9 @@ class TabbedBrowser(TabWidget):
|
||||
return
|
||||
last_close = config.get('tabbar', 'last-close')
|
||||
if self.count() > 1:
|
||||
self._url_stack.append(tab.url())
|
||||
url = tab.url()
|
||||
if not url.isEmpty():
|
||||
self._url_stack.append(url)
|
||||
self.removeTab(idx)
|
||||
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
|
||||
elif last_close == 'quit':
|
||||
@ -225,15 +228,16 @@ class TabbedBrowser(TabWidget):
|
||||
elif last_close == 'blank':
|
||||
tab.openurl('about:blank')
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs', split=False)
|
||||
def tabopen(self, url):
|
||||
@cmdutils.register(instance='mainwindow.tabs', split=False, name='tabopen')
|
||||
def tabopen_cmd(self, url):
|
||||
"""Open a new tab with a given url."""
|
||||
self._tabopen(url, background=False)
|
||||
self.tabopen(url, background=False)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs', split=False)
|
||||
def backtabopen(self, url):
|
||||
@cmdutils.register(instance='mainwindow.tabs', split=False,
|
||||
name='backtabopen')
|
||||
def backtabopen_cmd(self, url):
|
||||
"""Open a new tab in background."""
|
||||
self._tabopen(url, background=True)
|
||||
self.tabopen(url, background=True)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs', hide=True)
|
||||
def tabopencur(self):
|
||||
|
@ -19,11 +19,15 @@
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from urllib.error import URLError
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QClipboard
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout)
|
||||
QVBoxLayout, QHBoxLayout, QApplication)
|
||||
|
||||
import qutebrowser.config.config as config
|
||||
import qutebrowser.utils.misc as utils
|
||||
from qutebrowser.utils.version import version
|
||||
|
||||
|
||||
@ -39,6 +43,8 @@ class CrashDialog(QDialog):
|
||||
_hbox: The QHboxLayout containing the buttons
|
||||
_btn_quit: The quit button
|
||||
_btn_restore: the restore button
|
||||
_btn_pastebin: the pastebin button
|
||||
_url: Pastebin URL QLabel.
|
||||
"""
|
||||
|
||||
def __init__(self, pages, cmdhist, exc):
|
||||
@ -59,7 +65,7 @@ class CrashDialog(QDialog):
|
||||
text = ("Argh! qutebrowser crashed unexpectedly.<br/>"
|
||||
"Please review the info below to remove sensitive data and "
|
||||
"then submit it to <a href='mailto:crash@qutebrowser.org'>"
|
||||
"crash@qutebrowser.org</a>.<br/><br/>")
|
||||
"crash@qutebrowser.org</a> or click 'pastebin'.<br/><br/>")
|
||||
if pages:
|
||||
text += ("You can click \"Restore tabs\" to attempt to reopen "
|
||||
"your open tabs.")
|
||||
@ -68,15 +74,25 @@ class CrashDialog(QDialog):
|
||||
self._vbox.addWidget(self._lbl)
|
||||
|
||||
self._txt = QTextEdit()
|
||||
self._txt.setReadOnly(True)
|
||||
#self._txt.setReadOnly(True)
|
||||
self._txt.setText(self._crash_info(pages, cmdhist, exc))
|
||||
self._vbox.addWidget(self._txt)
|
||||
|
||||
self._url = QLabel()
|
||||
self._url.setTextInteractionFlags(Qt.TextSelectableByMouse |
|
||||
Qt.TextSelectableByKeyboard |
|
||||
Qt.LinksAccessibleByMouse |
|
||||
Qt.LinksAccessibleByKeyboard)
|
||||
self._vbox.addWidget(self._url)
|
||||
|
||||
self._hbox = QHBoxLayout()
|
||||
self._hbox.addStretch()
|
||||
self._btn_quit = QPushButton()
|
||||
self._btn_quit.setText("Quit")
|
||||
self._btn_quit.clicked.connect(self.reject)
|
||||
self._btn_pastebin = QPushButton()
|
||||
self._btn_pastebin.setText("Pastebin")
|
||||
self._btn_pastebin.clicked.connect(self.pastebin)
|
||||
self._hbox.addWidget(self._btn_quit)
|
||||
if pages:
|
||||
self._btn_restore = QPushButton()
|
||||
@ -84,6 +100,7 @@ class CrashDialog(QDialog):
|
||||
self._btn_restore.clicked.connect(self.accept)
|
||||
self._btn_restore.setDefault(True)
|
||||
self._hbox.addWidget(self._btn_restore)
|
||||
self._hbox.addWidget(self._btn_pastebin)
|
||||
|
||||
self._vbox.addLayout(self._hbox)
|
||||
|
||||
@ -116,3 +133,15 @@ class CrashDialog(QDialog):
|
||||
chunks.append('\n'.join([h, body]))
|
||||
|
||||
return '\n\n'.join(chunks)
|
||||
|
||||
def pastebin(self):
|
||||
"""Paste the crash info into the pastebin."""
|
||||
try:
|
||||
url = utils.pastebin(self._txt.toPlainText())
|
||||
except (URLError, ValueError) as e:
|
||||
self._url.setText('Error while pasting: {}'.format(e))
|
||||
return
|
||||
self._btn_pastebin.setEnabled(False)
|
||||
self._url.setText("URL copied to clipboard: "
|
||||
"<a href='{}'>{}</a>".format(url, url))
|
||||
QApplication.clipboard().setText(url, QClipboard.Clipboard)
|
||||
|
@ -208,12 +208,6 @@ class WebView(QWebView):
|
||||
self._destroyed[self] = False
|
||||
self.destroyed.connect(functools.partial(self._on_destroyed, self))
|
||||
self.deleteLater()
|
||||
|
||||
netman = QApplication.instance().networkmanager
|
||||
self._destroyed[netman] = False
|
||||
netman.abort_requests()
|
||||
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
|
||||
netman.deleteLater()
|
||||
logging.debug("Tab shutdown scheduled")
|
||||
|
||||
@pyqtSlot(str)
|
||||
@ -269,7 +263,9 @@ class WebView(QWebView):
|
||||
Args:
|
||||
target: A string to set self._force_open_target to.
|
||||
"""
|
||||
self._force_open_target = getattr(Target, target)
|
||||
t = getattr(Target, target)
|
||||
logging.debug("Setting force target to {}/{}".format(target, t))
|
||||
self._force_open_target = t
|
||||
|
||||
def paintEvent(self, e):
|
||||
"""Extend paintEvent to emit a signal if the scroll position changed.
|
||||
@ -338,14 +334,16 @@ class WebView(QWebView):
|
||||
self._open_target = self._force_open_target
|
||||
self._force_open_target = None
|
||||
logging.debug("Setting force target: {}".format(
|
||||
self._open_target))
|
||||
Target[self._open_target]))
|
||||
elif (e.button() == Qt.MidButton or
|
||||
e.modifiers() & Qt.ControlModifier):
|
||||
if config.get('general', 'background-tabs'):
|
||||
self._open_target = Target.bgtab
|
||||
else:
|
||||
self._open_target = Target.tab
|
||||
logging.debug("Setting target: {}".format(self._open_target))
|
||||
logging.debug("Middle click, setting target: {}".format(
|
||||
Target[self._open_target]))
|
||||
else:
|
||||
self._open_target = Target.normal
|
||||
logging.debug("Normal click, setting normal target")
|
||||
return super().mousePressEvent(e)
|
||||
|
@ -29,6 +29,7 @@ import sys
|
||||
import subprocess
|
||||
import os
|
||||
import os.path
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
try:
|
||||
@ -108,6 +109,13 @@ def check_pep257(args=None):
|
||||
print()
|
||||
|
||||
|
||||
def check_unittest():
|
||||
print("====== unittest ======")
|
||||
suite = unittest.TestLoader().discover('.')
|
||||
result = unittest.TextTestRunner().run(suite)
|
||||
print()
|
||||
status['unittest'] = result.wasSuccessful()
|
||||
|
||||
def check_line():
|
||||
"""Run _check_file over a filetree."""
|
||||
print("====== line ======")
|
||||
@ -198,6 +206,7 @@ def _get_args(checker):
|
||||
|
||||
if do_check_257:
|
||||
check_pep257(_get_args('pep257'))
|
||||
check_unittest()
|
||||
for checker in ['pylint', 'flake8', 'pyroma']:
|
||||
# FIXME what the hell is the flake8 exit status?
|
||||
run(checker, _get_args(checker))
|
||||
|
Loading…
Reference in New Issue
Block a user