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
|
- Chosing printer in :print gives us a segfault on Linux, but :printpreview
|
||||||
works...
|
works...
|
||||||
|
|
||||||
- Segfault when closing some tab:
|
- When following a hint:
|
||||||
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):
|
QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.
|
||||||
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
|
- When enabling javascript on http://google.com/accounts and reloading:
|
||||||
|
|
||||||
- Weird TypeError:
|
OpenType support missing for script 12
|
||||||
|
|
||||||
QIODevice::read: device not open
|
Then it quits.
|
||||||
QIODevice::read: device not open
|
When javascript is enabled already, hangs.
|
||||||
Traceback (most recent call last):
|
With minimal browser, prints error but continues to run.
|
||||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 567, in command_handler
|
|
||||||
handler(*args)
|
Also quits on http://de.wikipedia.org/wiki/Hallo-Welt-Programm
|
||||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/_tabbedbrowser.py", line 216, in tabclose
|
|
||||||
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
|
It seems this only happens on Windows.
|
||||||
File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/webview.py", line 215, in shutdown
|
|
||||||
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
|
- Closing some tabs and undo all them -> quits and CMD.exe hangs
|
||||||
TypeError: pyqtSignal must be bound to a QObject, not 'NetworkManager'
|
|
||||||
|
QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not dequeu request
|
||||||
|
|
||||||
@ e5000c315dd29ae9356e1b33ed041917c637c85b
|
|
||||||
|
|
||||||
- Sometimes self._frame is None in on_mode_left in HintManager
|
- Sometimes self._frame is None in on_mode_left in HintManager
|
||||||
|
|
||||||
@ -52,6 +44,24 @@ Bugs
|
|||||||
====
|
====
|
||||||
|
|
||||||
- All kind of FIXMEs
|
- 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
|
Style
|
||||||
=====
|
=====
|
||||||
@ -80,6 +90,9 @@ Major features
|
|||||||
Minor features
|
Minor features
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
- Enable disk caching
|
||||||
|
QNetworkManager.setCache() and use a QNetworkDiskCache probably
|
||||||
|
- clear cookies
|
||||||
- keybind/aliases should have completion for commands/arguments
|
- keybind/aliases should have completion for commands/arguments
|
||||||
- Hiding scrollbars
|
- Hiding scrollbars
|
||||||
- Ctrl+A/X to increase/decrease last number in URL
|
- Ctrl+A/X to increase/decrease last number in URL
|
||||||
@ -102,8 +115,15 @@ hints
|
|||||||
- filter close hints when it's the same link
|
- filter close hints when it's the same link
|
||||||
- ignore keypresses shortly after link following
|
- 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
|
- Printing under windows produced blank pages
|
||||||
https://bugreports.qt-project.org/browse/QTBUG-19571
|
https://bugreports.qt-project.org/browse/QTBUG-19571
|
||||||
@ -116,11 +136,54 @@ Qt Bugs
|
|||||||
https://bugreports.qt-project.org/browse/QTBUG-38669
|
https://bugreports.qt-project.org/browse/QTBUG-38669
|
||||||
|
|
||||||
- Web inspector is blank unless .hide()/.show() is called.
|
- Web inspector is blank unless .hide()/.show() is called.
|
||||||
|
Asked on SO: http://stackoverflow.com/q/23499159/2085149
|
||||||
|
|
||||||
- Weird font rendering
|
- Weird font rendering
|
||||||
https://bugreports.qt-project.org/browse/QTBUG-20973
|
https://bugreports.qt-project.org/browse/QTBUG-20973
|
||||||
https://bugreports.qt-project.org/browse/QTBUG-21036
|
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)
|
Keybinding stuff (from dwb)
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
|
@ -20,9 +20,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import functools
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import configparser
|
import configparser
|
||||||
|
from bdb import BdbQuit
|
||||||
|
from functools import partial
|
||||||
from signal import signal, SIGINT
|
from signal import signal, SIGINT
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
@ -102,7 +103,11 @@ class QuteBrowser(QApplication):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(sys.argv)
|
super().__init__(sys.argv)
|
||||||
self._quit_status = {}
|
self._quit_status = {
|
||||||
|
'crash': True,
|
||||||
|
'tabs': False,
|
||||||
|
'networkmanager': False
|
||||||
|
}
|
||||||
self._timers = []
|
self._timers = []
|
||||||
self._opened_urls = []
|
self._opened_urls = []
|
||||||
self._shutting_down = False
|
self._shutting_down = False
|
||||||
@ -392,15 +397,22 @@ class QuteBrowser(QApplication):
|
|||||||
exc = (exctype, excvalue, tb)
|
exc = (exctype, excvalue, tb)
|
||||||
sys.__excepthook__(*exc)
|
sys.__excepthook__(*exc)
|
||||||
|
|
||||||
if not issubclass(exctype, Exception):
|
self._quit_status['crash'] = False
|
||||||
# probably a KeyboardInterrupt
|
|
||||||
|
# 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:
|
try:
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
self.quit()
|
self.quit()
|
||||||
self._quit_status['crash'] = False
|
|
||||||
self._quit_status['shutdown'] = False
|
|
||||||
try:
|
try:
|
||||||
pages = self._recover_pages()
|
pages = self._recover_pages()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -483,49 +495,55 @@ class QuteBrowser(QApplication):
|
|||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@cmdutils.register(instance='', name=['quit', 'q'], nargs=0)
|
@cmdutils.register(instance='', name=['quit', 'q'], nargs=0)
|
||||||
def shutdown(self, do_quit=True):
|
def shutdown(self):
|
||||||
"""Try to shutdown everything cleanly.
|
"""Try to shutdown everything cleanly.
|
||||||
|
|
||||||
For some reason lastWindowClosing sometimes seem to get emitted twice,
|
For some reason lastWindowClosing sometimes seem to get emitted twice,
|
||||||
so we make sure we only run once here.
|
so we make sure we only run once here.
|
||||||
|
|
||||||
Args:
|
|
||||||
do_quit: Whether to quit after shutting down.
|
|
||||||
"""
|
"""
|
||||||
if self._shutting_down:
|
if self._shutting_down:
|
||||||
return
|
return
|
||||||
self._shutting_down = True
|
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'):
|
if self.config.get('general', 'auto-save-config'):
|
||||||
try:
|
try:
|
||||||
self.config.save()
|
self.config.save()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.exception("Could not save config.")
|
logging.exception("Could not save config.")
|
||||||
|
# Save command history
|
||||||
try:
|
try:
|
||||||
self.cmd_history.save()
|
self.cmd_history.save()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.exception("Could not save command history.")
|
logging.exception("Could not save command history.")
|
||||||
|
# Save window state
|
||||||
try:
|
try:
|
||||||
self._save_geometry()
|
self._save_geometry()
|
||||||
self.stateconfig.save()
|
self.stateconfig.save()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.exception("Could not save window geometry.")
|
logging.exception("Could not save window geometry.")
|
||||||
|
# Save cookies
|
||||||
try:
|
try:
|
||||||
self.cookiejar.save()
|
self.cookiejar.save()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.exception("Could not save cookies.")
|
logging.exception("Could not save cookies.")
|
||||||
|
# Shut down tabs
|
||||||
try:
|
try:
|
||||||
if do_quit:
|
self.mainwindow.tabs.shutdown_complete.connect(partial(
|
||||||
self.mainwindow.tabs.shutdown_complete.connect(
|
self._maybe_quit, 'tabs'))
|
||||||
self.on_tab_shutdown_complete)
|
|
||||||
else:
|
|
||||||
self.mainwindow.tabs.shutdown_complete.connect(
|
|
||||||
functools.partial(self._maybe_quit, 'shutdown'))
|
|
||||||
self.mainwindow.tabs.shutdown()
|
self.mainwindow.tabs.shutdown()
|
||||||
except AttributeError: # mainwindow or tabs could still be None
|
except AttributeError: # mainwindow or tabs could still be None
|
||||||
logging.exception("No mainwindow/tabs to shut down.")
|
logging.exception("No mainwindow/tabs to shut down.")
|
||||||
if do_quit:
|
self._maybe_quit('tabs')
|
||||||
self.quit()
|
# 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()
|
@pyqtSlot()
|
||||||
def on_tab_shutdown_complete(self):
|
def on_tab_shutdown_complete(self):
|
||||||
|
@ -21,7 +21,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
from collections import namedtuple
|
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.QtGui import QMouseEvent, QClipboard
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
@ -70,6 +70,7 @@ class HintManager(QObject):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
HINT_CSS = """
|
HINT_CSS = """
|
||||||
|
display: {display};
|
||||||
color: {config[colors][hints.fg]};
|
color: {config[colors][hints.fg]};
|
||||||
background: {config[colors][hints.bg]};
|
background: {config[colors][hints.bg]};
|
||||||
font: {config[fonts][hints]};
|
font: {config[fonts][hints]};
|
||||||
@ -193,6 +194,24 @@ class HintManager(QObject):
|
|||||||
hintstr.insert(0, chars[0])
|
hintstr.insert(0, chars[0])
|
||||||
return ''.join(hintstr)
|
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):
|
def _draw_label(self, elem, string):
|
||||||
"""Draw a hint label over an element.
|
"""Draw a hint label over an element.
|
||||||
|
|
||||||
@ -203,9 +222,7 @@ class HintManager(QObject):
|
|||||||
Return:
|
Return:
|
||||||
The newly created label elment
|
The newly created label elment
|
||||||
"""
|
"""
|
||||||
rect = elem.geometry()
|
css = self._get_hint_css(elem)
|
||||||
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
|
||||||
config=config.instance())
|
|
||||||
doc = self._frame.documentElement()
|
doc = self._frame.documentElement()
|
||||||
# It seems impossible to create an empty QWebElement for which isNull()
|
# It seems impossible to create an empty QWebElement for which isNull()
|
||||||
# is false so we can work with it.
|
# is false so we can work with it.
|
||||||
@ -227,7 +244,13 @@ class HintManager(QObject):
|
|||||||
else:
|
else:
|
||||||
target = self._target
|
target = self._target
|
||||||
self.set_open_target.emit(Target[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()
|
scrollpos = self._frame.scrollPosition()
|
||||||
logging.debug("Clicking on \"{}\" at {}/{} - {}/{}".format(
|
logging.debug("Clicking on \"{}\" at {}/{} - {}/{}".format(
|
||||||
elem.toPlainText(), point.x(), point.y(), scrollpos.x(),
|
elem.toPlainText(), point.x(), point.y(), scrollpos.x(),
|
||||||
@ -382,34 +405,48 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
def handle_partial_key(self, keystr):
|
def handle_partial_key(self, keystr):
|
||||||
"""Handle a new partial keypress."""
|
"""Handle a new partial keypress."""
|
||||||
delete = []
|
logging.debug("Handling new keystring: '{}'".format(keystr))
|
||||||
for (string, elems) in self._elems.items():
|
for (string, elems) in self._elems.items():
|
||||||
if string.startswith(keystr):
|
if string.startswith(keystr):
|
||||||
matched = string[:len(keystr)]
|
matched = string[:len(keystr)]
|
||||||
rest = string[len(keystr):]
|
rest = string[len(keystr):]
|
||||||
elems.label.setInnerXml('<font color="{}">{}</font>{}'.format(
|
elems.label.setInnerXml('<font color="{}">{}</font>{}'.format(
|
||||||
config.get('colors', 'hints.fg.match'), matched, rest))
|
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:
|
else:
|
||||||
elems.label.removeFromDocument()
|
# element doesn't match anymore -> hide it
|
||||||
delete.append(string)
|
elems.label.setAttribute('hidden', 'true')
|
||||||
for key in delete:
|
css = self._get_hint_css(elems.elem, elems.label)
|
||||||
del self._elems[key]
|
elems.label.setAttribute('style', css)
|
||||||
|
|
||||||
def filter_hints(self, filterstr):
|
def filter_hints(self, filterstr):
|
||||||
"""Filter displayed hints according to a text."""
|
"""Filter displayed hints according to a text."""
|
||||||
delete = []
|
for elems in self._elems.values():
|
||||||
for (string, elems) in self._elems.items():
|
if elems.elem.toPlainText().lower().startswith(filterstr):
|
||||||
if not elems.elem.toPlainText().lower().startswith(filterstr):
|
if elems.label.attribute('hidden') == 'true':
|
||||||
elems.label.removeFromDocument()
|
# hidden element which matches again -> unhide it
|
||||||
delete.append(string)
|
elems.label.setAttribute('hidden', 'false')
|
||||||
for key in delete:
|
css = self._get_hint_css(elems.elem, elems.label)
|
||||||
del self._elems[key]
|
elems.label.setAttribute('style', css)
|
||||||
if not self._elems:
|
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
|
# Whoops, filtered all hints
|
||||||
modeman.leave('hint')
|
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.
|
# unpacking gets us the first (and only) key in the dict.
|
||||||
self.fire(*self._elems)
|
self.fire(*visible)
|
||||||
|
|
||||||
def fire(self, keystr, force=False):
|
def fire(self, keystr, force=False):
|
||||||
"""Fire a completed hint.
|
"""Fire a completed hint.
|
||||||
@ -461,9 +498,7 @@ class HintManager(QObject):
|
|||||||
def on_contents_size_changed(self, _size):
|
def on_contents_size_changed(self, _size):
|
||||||
"""Reposition hints if contents size changed."""
|
"""Reposition hints if contents size changed."""
|
||||||
for elems in self._elems.values():
|
for elems in self._elems.values():
|
||||||
rect = elems.elem.geometry()
|
css = self._get_hint_css(elems.elem, elems.label)
|
||||||
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
|
|
||||||
config=config.instance())
|
|
||||||
elems.label.setAttribute('style', css)
|
elems.label.setAttribute('style', css)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
@ -472,9 +507,14 @@ class HintManager(QObject):
|
|||||||
if mode != 'hint':
|
if mode != 'hint':
|
||||||
return
|
return
|
||||||
for elem in self._elems.values():
|
for elem in self._elems.values():
|
||||||
elem.label.removeFromDocument()
|
if not elem.label.isNull():
|
||||||
self._frame.contentsSizeChanged.disconnect(
|
elem.label.removeFromDocument()
|
||||||
self.on_contents_size_changed)
|
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._elems = {}
|
||||||
self._to_follow = None
|
self._to_follow = None
|
||||||
self._target = None
|
self._target = None
|
||||||
|
@ -72,7 +72,7 @@ class BrowserPage(QWebPage):
|
|||||||
def userAgentForUrl(self, url):
|
def userAgentForUrl(self, url):
|
||||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||||
ua = config.get('network', 'user-agent')
|
ua = config.get('network', 'user-agent')
|
||||||
if not ua:
|
if ua is None:
|
||||||
return super().userAgentForUrl(url)
|
return super().userAgentForUrl(url)
|
||||||
else:
|
else:
|
||||||
return ua
|
return ua
|
||||||
|
@ -155,17 +155,22 @@ class String(BaseType):
|
|||||||
Attributes:
|
Attributes:
|
||||||
minlen: Minimum length (inclusive).
|
minlen: Minimum length (inclusive).
|
||||||
maxlen: Maximum length (inclusive).
|
maxlen: Maximum length (inclusive).
|
||||||
|
forbidden: Forbidden chars in the string.
|
||||||
|
none: Whether to convert to None for an empty string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
typestr = '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.minlen = minlen
|
||||||
self.maxlen = maxlen
|
self.maxlen = maxlen
|
||||||
self.forbidden = forbidden
|
self.forbidden = forbidden
|
||||||
|
self.none = none
|
||||||
|
|
||||||
def transform(self, value):
|
def transform(self, value):
|
||||||
return value.lower()
|
if self.none and not value:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if self.forbidden is not None and any(c in value
|
if self.forbidden is not None and any(c in value
|
||||||
@ -180,37 +185,17 @@ class String(BaseType):
|
|||||||
self.maxlen))
|
self.maxlen))
|
||||||
|
|
||||||
|
|
||||||
class ShellCommand(String):
|
class List(BaseType):
|
||||||
|
|
||||||
"""A shellcommand which is split via shlex.
|
"""Base class for a (string-)list setting."""
|
||||||
|
|
||||||
Attributes:
|
typestr = 'string-list'
|
||||||
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.")
|
|
||||||
|
|
||||||
def transform(self, value):
|
def transform(self, value):
|
||||||
return shlex.split(value)
|
return value.split(',')
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
class HintMode(BaseType):
|
pass
|
||||||
|
|
||||||
"""Base class for the hints -> mode setting."""
|
|
||||||
|
|
||||||
typestr = 'hint-mode'
|
|
||||||
|
|
||||||
valid_values = ValidValues(('number', "Use numeric hints."),
|
|
||||||
('letter', "Use the chars in hints -> chars."))
|
|
||||||
|
|
||||||
|
|
||||||
class Bool(BaseType):
|
class Bool(BaseType):
|
||||||
@ -245,18 +230,24 @@ class Int(BaseType):
|
|||||||
Attributes:
|
Attributes:
|
||||||
minval: Minimum value (inclusive).
|
minval: Minimum value (inclusive).
|
||||||
maxval: Maximum value (inclusive).
|
maxval: Maximum value (inclusive).
|
||||||
|
none: Whether to accept empty values as None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
typestr = 'int'
|
typestr = 'int'
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None):
|
def __init__(self, minval=None, maxval=None, none=False):
|
||||||
self.minval = minval
|
self.minval = minval
|
||||||
self.maxval = maxval
|
self.maxval = maxval
|
||||||
|
self.none = none
|
||||||
|
|
||||||
def transform(self, value):
|
def transform(self, value):
|
||||||
|
if self.none and not value:
|
||||||
|
return None
|
||||||
return int(value)
|
return int(value)
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
|
if self.none and not value:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
intval = int(value)
|
intval = int(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -269,20 +260,21 @@ class Int(BaseType):
|
|||||||
self.maxval))
|
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):
|
def transform(self, value):
|
||||||
if value == '':
|
vals = super().transform(value)
|
||||||
return None
|
return map(int, vals)
|
||||||
else:
|
|
||||||
return super().transform(value)
|
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if value == '':
|
try:
|
||||||
return
|
self.transform(value)
|
||||||
super().validate(value)
|
except ValueError:
|
||||||
|
raise ValidationError(value, "must be a list of integers!")
|
||||||
|
|
||||||
|
|
||||||
class Float(BaseType):
|
class Float(BaseType):
|
||||||
@ -316,36 +308,6 @@ class Float(BaseType):
|
|||||||
self.maxval))
|
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):
|
class Perc(BaseType):
|
||||||
|
|
||||||
"""Percentage.
|
"""Percentage.
|
||||||
@ -355,6 +317,8 @@ class Perc(BaseType):
|
|||||||
maxval: Maximum value (inclusive).
|
maxval: Maximum value (inclusive).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
typestr = 'percentage'
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None):
|
def __init__(self, minval=None, maxval=None):
|
||||||
self.minval = minval
|
self.minval = minval
|
||||||
self.maxval = maxval
|
self.maxval = maxval
|
||||||
@ -406,15 +370,6 @@ class PercList(List):
|
|||||||
raise ValidationError(value, "must be a list of percentages!")
|
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):
|
class PercOrInt(BaseType):
|
||||||
|
|
||||||
"""Percentage or integer.
|
"""Percentage or integer.
|
||||||
@ -426,6 +381,8 @@ class PercOrInt(BaseType):
|
|||||||
maxint: Maximum value for integer (inclusive).
|
maxint: Maximum value for integer (inclusive).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
typestr = 'percentage-or-int'
|
||||||
|
|
||||||
def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None):
|
def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None):
|
||||||
self.minperc = minperc
|
self.minperc = minperc
|
||||||
self.maxperc = maxperc
|
self.maxperc = maxperc
|
||||||
@ -457,130 +414,6 @@ class PercOrInt(BaseType):
|
|||||||
self.maxint))
|
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):
|
class Command(BaseType):
|
||||||
|
|
||||||
"""Base class for a command value with arguments."""
|
"""Base class for a command value with arguments."""
|
||||||
@ -646,6 +479,225 @@ class Font(BaseType):
|
|||||||
pass
|
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):
|
class SearchEngineName(BaseType):
|
||||||
|
|
||||||
"""A search engine name."""
|
"""A search engine name."""
|
||||||
@ -673,55 +725,19 @@ class KeyBindingName(BaseType):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Regex(BaseType):
|
class KeyBinding(Command):
|
||||||
|
|
||||||
"""A regular expression."""
|
"""The command of a keybinding."""
|
||||||
|
|
||||||
def __init__(self, flags=0):
|
pass
|
||||||
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!")
|
|
||||||
|
|
||||||
|
|
||||||
class WebSettingsFile(File):
|
class WebSettingsFile(File):
|
||||||
|
|
||||||
"""QWebSettings file which also can be none."""
|
"""QWebSettings file which also can be none."""
|
||||||
|
|
||||||
|
typestr = 'file'
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if value == '':
|
if value == '':
|
||||||
# empty values are okay
|
# empty values are okay
|
||||||
@ -791,10 +807,3 @@ class AcceptCookies(String):
|
|||||||
|
|
||||||
valid_values = ValidValues(('default', "Default QtWebKit behaviour"),
|
valid_values = ValidValues(('default', "Default QtWebKit behaviour"),
|
||||||
('never', "Don't accept cookies at all."))
|
('never', "Don't accept cookies at all."))
|
||||||
|
|
||||||
|
|
||||||
class KeyBinding(Command):
|
|
||||||
|
|
||||||
"""The command of a keybinding."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
@ -271,7 +271,6 @@ class ConfigManager(QObject):
|
|||||||
Return:
|
Return:
|
||||||
The value of the option.
|
The value of the option.
|
||||||
"""
|
"""
|
||||||
logging.debug("getting {} -> {}".format(sectname, optname))
|
|
||||||
try:
|
try:
|
||||||
sect = self.sections[sectname]
|
sect = self.sections[sectname]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -285,7 +284,6 @@ class ConfigManager(QObject):
|
|||||||
mapping = {key: val.value for key, val in sect.values.items()}
|
mapping = {key: val.value for key, val in sect.values.items()}
|
||||||
newval = self._interpolation.before_get(self, sectname, optname,
|
newval = self._interpolation.before_get(self, sectname, optname,
|
||||||
val.value, mapping)
|
val.value, mapping)
|
||||||
logging.debug("interpolated val: {}".format(newval))
|
|
||||||
if transformed:
|
if transformed:
|
||||||
newval = val.typ.transform(newval)
|
newval = val.typ.transform(newval)
|
||||||
return newval
|
return newval
|
||||||
|
@ -203,11 +203,11 @@ DATA = OrderedDict([
|
|||||||
"Value to send in the DNT header."),
|
"Value to send in the DNT header."),
|
||||||
|
|
||||||
('accept-language',
|
('accept-language',
|
||||||
SettingValue(types.String(), 'en-US,en'),
|
SettingValue(types.String(none=True), 'en-US,en'),
|
||||||
"Value to send in the accept-language header."),
|
"Value to send in the accept-language header."),
|
||||||
|
|
||||||
('user-agent',
|
('user-agent',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"User agent to send. Empty to send the default."),
|
"User agent to send. Empty to send the default."),
|
||||||
|
|
||||||
('accept-cookies',
|
('accept-cookies',
|
||||||
@ -422,55 +422,55 @@ DATA = OrderedDict([
|
|||||||
"User stylesheet to set."),
|
"User stylesheet to set."),
|
||||||
|
|
||||||
('css-media-type',
|
('css-media-type',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Set the CSS media type."),
|
"Set the CSS media type."),
|
||||||
|
|
||||||
('default-encoding',
|
('default-encoding',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Default encoding to use for websites."),
|
"Default encoding to use for websites."),
|
||||||
|
|
||||||
('font-family-standard',
|
('font-family-standard',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Font family for standard fonts."),
|
"Font family for standard fonts."),
|
||||||
|
|
||||||
('font-family-fixed',
|
('font-family-fixed',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Font family for fixed fonts."),
|
"Font family for fixed fonts."),
|
||||||
|
|
||||||
('font-family-serif',
|
('font-family-serif',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Font family for serif fonts."),
|
"Font family for serif fonts."),
|
||||||
|
|
||||||
('font-family-sans-serif',
|
('font-family-sans-serif',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Font family for sans-serif fonts."),
|
"Font family for sans-serif fonts."),
|
||||||
|
|
||||||
('font-family-cursive',
|
('font-family-cursive',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Font family for cursive fonts."),
|
"Font family for cursive fonts."),
|
||||||
|
|
||||||
('font-family-fantasy',
|
('font-family-fantasy',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"Font family for fantasy fonts."),
|
"Font family for fantasy fonts."),
|
||||||
|
|
||||||
('font-size-minimum',
|
('font-size-minimum',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"The hard minimum font size."),
|
"The hard minimum font size."),
|
||||||
|
|
||||||
('font-size-minimum-logical',
|
('font-size-minimum-logical',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"The minimum logical font size that is applied when zooming out."),
|
"The minimum logical font size that is applied when zooming out."),
|
||||||
|
|
||||||
('font-size-default',
|
('font-size-default',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"The default font size for regular text."),
|
"The default font size for regular text."),
|
||||||
|
|
||||||
('font-size-default-fixed',
|
('font-size-default-fixed',
|
||||||
SettingValue(types.String(), ''),
|
SettingValue(types.String(none=True), ''),
|
||||||
"The default font size for fixed-pitch text."),
|
"The default font size for fixed-pitch text."),
|
||||||
|
|
||||||
('maximum-pages-in-cache',
|
('maximum-pages-in-cache',
|
||||||
SettingValue(types.NoneInt(), ''),
|
SettingValue(types.Int(none=True), ''),
|
||||||
"The default font size for fixed-pitch text."),
|
"The default font size for fixed-pitch text."),
|
||||||
|
|
||||||
('object-cache-capacities',
|
('object-cache-capacities',
|
||||||
@ -485,7 +485,7 @@ DATA = OrderedDict([
|
|||||||
|
|
||||||
('offline-web-application-cache-quota',
|
('offline-web-application-cache-quota',
|
||||||
SettingValue(types.WebKitBytes(maxsize=INT64_MAX), ''),
|
SettingValue(types.WebKitBytes(maxsize=INT64_MAX), ''),
|
||||||
"Quota for the offline web application cache>"),
|
"Quota for the offline web application cache."),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
('hints', sect.KeyValue(
|
('hints', sect.KeyValue(
|
||||||
|
@ -150,7 +150,8 @@ class BaseKeyParser(QObject):
|
|||||||
"""
|
"""
|
||||||
logging.debug("Got key: {} / text: '{}'".format(e.key(), e.text()))
|
logging.debug("Got key: {} / text: '{}'".format(e.key(), e.text()))
|
||||||
txt = e.text().strip()
|
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")
|
logging.debug("Ignoring, no text")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -172,9 +173,13 @@ class BaseKeyParser(QObject):
|
|||||||
(match, binding) = self._match_key(cmd_input)
|
(match, binding) = self._match_key(cmd_input)
|
||||||
|
|
||||||
if match == self.Match.definitive:
|
if match == self.Match.definitive:
|
||||||
|
logging.debug("Definitive match for "
|
||||||
|
"\"{}\".".format(self._keystring))
|
||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
self.execute(binding, self.Type.chain, count)
|
self.execute(binding, self.Type.chain, count)
|
||||||
elif match == self.Match.ambiguous:
|
elif match == self.Match.ambiguous:
|
||||||
|
logging.debug("Ambigious match for "
|
||||||
|
"\"{}\".".format(self._keystring))
|
||||||
self._handle_ambiguous_match(binding, count)
|
self._handle_ambiguous_match(binding, count)
|
||||||
elif match == self.Match.partial:
|
elif match == self.Match.partial:
|
||||||
logging.debug("No match for \"{}\" (added {})".format(
|
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.utils.message as message
|
||||||
import qutebrowser.config.config as config
|
import qutebrowser.config.config as config
|
||||||
from qutebrowser.keyinput.keyparser import CommandKeyParser
|
from qutebrowser.keyinput.keyparser import CommandKeyParser
|
||||||
|
from qutebrowser.utils.usertypes import enum
|
||||||
|
|
||||||
|
|
||||||
STARTCHARS = ":/?"
|
STARTCHARS = ":/?"
|
||||||
|
LastPress = enum('none', 'filtertext', 'keystring')
|
||||||
|
|
||||||
|
|
||||||
class NormalKeyParser(CommandKeyParser):
|
class NormalKeyParser(CommandKeyParser):
|
||||||
@ -69,6 +71,7 @@ class HintKeyParser(CommandKeyParser):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_filtertext: The text to filter with.
|
_filtertext: The text to filter with.
|
||||||
|
_last_press: The nature of the last keypress, a LastPress member.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fire_hint = pyqtSignal(str)
|
fire_hint = pyqtSignal(str)
|
||||||
@ -77,6 +80,7 @@ class HintKeyParser(CommandKeyParser):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent, supports_count=False, supports_chains=True)
|
super().__init__(parent, supports_count=False, supports_chains=True)
|
||||||
self._filtertext = ''
|
self._filtertext = ''
|
||||||
|
self._last_press = LastPress.none
|
||||||
self.read_config('keybind.hint')
|
self.read_config('keybind.hint')
|
||||||
|
|
||||||
def _handle_special_key(self, e):
|
def _handle_special_key(self, e):
|
||||||
@ -92,20 +96,32 @@ class HintKeyParser(CommandKeyParser):
|
|||||||
|
|
||||||
Emit:
|
Emit:
|
||||||
filter_hints: Emitted when filter string has changed.
|
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()))
|
logging.debug("Got special key {} text {}".format(e.key(), e.text()))
|
||||||
if config.get('hints', 'mode') != 'number':
|
if e.key() == Qt.Key_Backspace:
|
||||||
return super()._handle_special_key(e)
|
logging.debug("Got backspace, mode {}, filtertext \"{}\", "
|
||||||
elif e.key() == Qt.Key_Backspace:
|
"keystring \"{}\"".format(
|
||||||
if self._filtertext:
|
LastPress[self._last_press], self._filtertext,
|
||||||
|
self._keystring))
|
||||||
|
if self._last_press == LastPress.filtertext and self._filtertext:
|
||||||
self._filtertext = self._filtertext[:-1]
|
self._filtertext = self._filtertext[:-1]
|
||||||
self.filter_hints.emit(self._filtertext)
|
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():
|
elif not e.text():
|
||||||
return super()._handle_special_key(e)
|
return super()._handle_special_key(e)
|
||||||
else:
|
else:
|
||||||
self._filtertext += e.text()
|
self._filtertext += e.text()
|
||||||
self.filter_hints.emit(self._filtertext)
|
self.filter_hints.emit(self._filtertext)
|
||||||
|
self._last_press = LastPress.filtertext
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def handle(self, e):
|
def handle(self, e):
|
||||||
@ -120,6 +136,7 @@ class HintKeyParser(CommandKeyParser):
|
|||||||
handled = self._handle_single_key(e)
|
handled = self._handle_single_key(e)
|
||||||
if handled:
|
if handled:
|
||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(self._keystring)
|
||||||
|
self._last_press = LastPress.keystring
|
||||||
return handled
|
return handled
|
||||||
return self._handle_special_key(e)
|
return self._handle_special_key(e)
|
||||||
|
|
||||||
|
@ -72,8 +72,10 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
dnt = '0'.encode('ascii')
|
dnt = '0'.encode('ascii')
|
||||||
req.setRawHeader('DNT'.encode('ascii'), dnt)
|
req.setRawHeader('DNT'.encode('ascii'), dnt)
|
||||||
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
|
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
|
||||||
req.setRawHeader('Accept-Language'.encode('ascii'),
|
accept_language = config.get('network', '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)
|
reply = super().createRequest(op, req, outgoing_data)
|
||||||
self._requests[id(reply)] = reply
|
self._requests[id(reply)] = reply
|
||||||
reply.destroyed.connect(lambda obj: self._requests.pop(id(obj)))
|
reply.destroyed.connect(lambda obj: self._requests.pop(id(obj)))
|
||||||
|
@ -137,6 +137,7 @@ class IsUrlNaiveTests(TestCase):
|
|||||||
'foo bar',
|
'foo bar',
|
||||||
'localhost test',
|
'localhost test',
|
||||||
'another . test',
|
'another . test',
|
||||||
|
'foo',
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_urls(self):
|
def test_urls(self):
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import shlex
|
import shlex
|
||||||
|
import urllib.request
|
||||||
|
from urllib.parse import urljoin, urlencode
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pkg_resources import resource_string
|
from pkg_resources import resource_string
|
||||||
|
|
||||||
@ -110,3 +112,24 @@ def shell_escape(s):
|
|||||||
# use single quotes, and put single quotes into double quotes
|
# use single quotes, and put single quotes into double quotes
|
||||||
# the string $'b is then quoted as '$'"'"'b'
|
# the string $'b is then quoted as '$'"'"'b'
|
||||||
return "'" + s.replace("'", "'\"'\"'") + "'"
|
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.
|
True if the url really is an URL, False otherwise.
|
||||||
"""
|
"""
|
||||||
protocols = ['http', 'https']
|
protocols = ['http', 'https']
|
||||||
|
u = qurl(url)
|
||||||
|
urlstr = urlstring(url)
|
||||||
if isinstance(url, QUrl):
|
if isinstance(url, QUrl):
|
||||||
u = url
|
u = url
|
||||||
else:
|
else:
|
||||||
u = QUrl.fromUserInput(url)
|
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
|
return True
|
||||||
elif '.' in u.host():
|
elif '.' in u.host():
|
||||||
return True
|
return True
|
||||||
|
@ -139,7 +139,8 @@ class TabbedBrowser(TabWidget):
|
|||||||
tab.open_tab.connect(self.tabopen)
|
tab.open_tab.connect(self.tabopen)
|
||||||
tab.iconChanged.connect(self.on_icon_changed)
|
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.
|
"""Open a new tab with a given url.
|
||||||
|
|
||||||
Inner logic for tabopen and backtabopen.
|
Inner logic for tabopen and backtabopen.
|
||||||
@ -217,7 +218,9 @@ class TabbedBrowser(TabWidget):
|
|||||||
return
|
return
|
||||||
last_close = config.get('tabbar', 'last-close')
|
last_close = config.get('tabbar', 'last-close')
|
||||||
if self.count() > 1:
|
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)
|
self.removeTab(idx)
|
||||||
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
|
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
|
||||||
elif last_close == 'quit':
|
elif last_close == 'quit':
|
||||||
@ -225,15 +228,16 @@ class TabbedBrowser(TabWidget):
|
|||||||
elif last_close == 'blank':
|
elif last_close == 'blank':
|
||||||
tab.openurl('about:blank')
|
tab.openurl('about:blank')
|
||||||
|
|
||||||
@cmdutils.register(instance='mainwindow.tabs', split=False)
|
@cmdutils.register(instance='mainwindow.tabs', split=False, name='tabopen')
|
||||||
def tabopen(self, url):
|
def tabopen_cmd(self, url):
|
||||||
"""Open a new tab with a given 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)
|
@cmdutils.register(instance='mainwindow.tabs', split=False,
|
||||||
def backtabopen(self, url):
|
name='backtabopen')
|
||||||
|
def backtabopen_cmd(self, url):
|
||||||
"""Open a new tab in background."""
|
"""Open a new tab in background."""
|
||||||
self._tabopen(url, background=True)
|
self.tabopen(url, background=True)
|
||||||
|
|
||||||
@cmdutils.register(instance='mainwindow.tabs', hide=True)
|
@cmdutils.register(instance='mainwindow.tabs', hide=True)
|
||||||
def tabopencur(self):
|
def tabopencur(self):
|
||||||
|
@ -19,11 +19,15 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
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,
|
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||||
QVBoxLayout, QHBoxLayout)
|
QVBoxLayout, QHBoxLayout, QApplication)
|
||||||
|
|
||||||
import qutebrowser.config.config as config
|
import qutebrowser.config.config as config
|
||||||
|
import qutebrowser.utils.misc as utils
|
||||||
from qutebrowser.utils.version import version
|
from qutebrowser.utils.version import version
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +43,8 @@ class CrashDialog(QDialog):
|
|||||||
_hbox: The QHboxLayout containing the buttons
|
_hbox: The QHboxLayout containing the buttons
|
||||||
_btn_quit: The quit button
|
_btn_quit: The quit button
|
||||||
_btn_restore: the restore button
|
_btn_restore: the restore button
|
||||||
|
_btn_pastebin: the pastebin button
|
||||||
|
_url: Pastebin URL QLabel.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pages, cmdhist, exc):
|
def __init__(self, pages, cmdhist, exc):
|
||||||
@ -59,7 +65,7 @@ class CrashDialog(QDialog):
|
|||||||
text = ("Argh! qutebrowser crashed unexpectedly.<br/>"
|
text = ("Argh! qutebrowser crashed unexpectedly.<br/>"
|
||||||
"Please review the info below to remove sensitive data and "
|
"Please review the info below to remove sensitive data and "
|
||||||
"then submit it to <a href='mailto:crash@qutebrowser.org'>"
|
"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:
|
if pages:
|
||||||
text += ("You can click \"Restore tabs\" to attempt to reopen "
|
text += ("You can click \"Restore tabs\" to attempt to reopen "
|
||||||
"your open tabs.")
|
"your open tabs.")
|
||||||
@ -68,15 +74,25 @@ class CrashDialog(QDialog):
|
|||||||
self._vbox.addWidget(self._lbl)
|
self._vbox.addWidget(self._lbl)
|
||||||
|
|
||||||
self._txt = QTextEdit()
|
self._txt = QTextEdit()
|
||||||
self._txt.setReadOnly(True)
|
#self._txt.setReadOnly(True)
|
||||||
self._txt.setText(self._crash_info(pages, cmdhist, exc))
|
self._txt.setText(self._crash_info(pages, cmdhist, exc))
|
||||||
self._vbox.addWidget(self._txt)
|
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 = QHBoxLayout()
|
||||||
self._hbox.addStretch()
|
self._hbox.addStretch()
|
||||||
self._btn_quit = QPushButton()
|
self._btn_quit = QPushButton()
|
||||||
self._btn_quit.setText("Quit")
|
self._btn_quit.setText("Quit")
|
||||||
self._btn_quit.clicked.connect(self.reject)
|
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)
|
self._hbox.addWidget(self._btn_quit)
|
||||||
if pages:
|
if pages:
|
||||||
self._btn_restore = QPushButton()
|
self._btn_restore = QPushButton()
|
||||||
@ -84,6 +100,7 @@ class CrashDialog(QDialog):
|
|||||||
self._btn_restore.clicked.connect(self.accept)
|
self._btn_restore.clicked.connect(self.accept)
|
||||||
self._btn_restore.setDefault(True)
|
self._btn_restore.setDefault(True)
|
||||||
self._hbox.addWidget(self._btn_restore)
|
self._hbox.addWidget(self._btn_restore)
|
||||||
|
self._hbox.addWidget(self._btn_pastebin)
|
||||||
|
|
||||||
self._vbox.addLayout(self._hbox)
|
self._vbox.addLayout(self._hbox)
|
||||||
|
|
||||||
@ -116,3 +133,15 @@ class CrashDialog(QDialog):
|
|||||||
chunks.append('\n'.join([h, body]))
|
chunks.append('\n'.join([h, body]))
|
||||||
|
|
||||||
return '\n\n'.join(chunks)
|
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[self] = False
|
||||||
self.destroyed.connect(functools.partial(self._on_destroyed, self))
|
self.destroyed.connect(functools.partial(self._on_destroyed, self))
|
||||||
self.deleteLater()
|
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")
|
logging.debug("Tab shutdown scheduled")
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
@ -269,7 +263,9 @@ class WebView(QWebView):
|
|||||||
Args:
|
Args:
|
||||||
target: A string to set self._force_open_target to.
|
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):
|
def paintEvent(self, e):
|
||||||
"""Extend paintEvent to emit a signal if the scroll position changed.
|
"""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._open_target = self._force_open_target
|
||||||
self._force_open_target = None
|
self._force_open_target = None
|
||||||
logging.debug("Setting force target: {}".format(
|
logging.debug("Setting force target: {}".format(
|
||||||
self._open_target))
|
Target[self._open_target]))
|
||||||
elif (e.button() == Qt.MidButton or
|
elif (e.button() == Qt.MidButton or
|
||||||
e.modifiers() & Qt.ControlModifier):
|
e.modifiers() & Qt.ControlModifier):
|
||||||
if config.get('general', 'background-tabs'):
|
if config.get('general', 'background-tabs'):
|
||||||
self._open_target = Target.bgtab
|
self._open_target = Target.bgtab
|
||||||
else:
|
else:
|
||||||
self._open_target = Target.tab
|
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:
|
else:
|
||||||
self._open_target = Target.normal
|
self._open_target = Target.normal
|
||||||
|
logging.debug("Normal click, setting normal target")
|
||||||
return super().mousePressEvent(e)
|
return super().mousePressEvent(e)
|
||||||
|
@ -29,6 +29,7 @@ import sys
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import unittest
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -108,6 +109,13 @@ def check_pep257(args=None):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def check_unittest():
|
||||||
|
print("====== unittest ======")
|
||||||
|
suite = unittest.TestLoader().discover('.')
|
||||||
|
result = unittest.TextTestRunner().run(suite)
|
||||||
|
print()
|
||||||
|
status['unittest'] = result.wasSuccessful()
|
||||||
|
|
||||||
def check_line():
|
def check_line():
|
||||||
"""Run _check_file over a filetree."""
|
"""Run _check_file over a filetree."""
|
||||||
print("====== line ======")
|
print("====== line ======")
|
||||||
@ -198,6 +206,7 @@ def _get_args(checker):
|
|||||||
|
|
||||||
if do_check_257:
|
if do_check_257:
|
||||||
check_pep257(_get_args('pep257'))
|
check_pep257(_get_args('pep257'))
|
||||||
|
check_unittest()
|
||||||
for checker in ['pylint', 'flake8', 'pyroma']:
|
for checker in ['pylint', 'flake8', 'pyroma']:
|
||||||
# FIXME what the hell is the flake8 exit status?
|
# FIXME what the hell is the flake8 exit status?
|
||||||
run(checker, _get_args(checker))
|
run(checker, _get_args(checker))
|
||||||
|
Loading…
Reference in New Issue
Block a user