Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130d2cb33b | ||
|
|
d36a0d5d15 | ||
|
|
a126924c42 | ||
|
|
a0381e1683 | ||
|
|
891bb86175 | ||
|
|
d9f356652f | ||
|
|
a4a6099515 | ||
|
|
44dd4da33f | ||
|
|
f69470ddcd | ||
|
|
71dbdb37a2 | ||
|
|
6c0e470b60 | ||
|
|
2322ee4f2c | ||
|
|
669760ed8f | ||
|
|
2b34fbc073 | ||
|
|
e8b689ab50 | ||
|
|
20c3e8dd52 | ||
|
|
4ec618386b | ||
|
|
05f5083c9c | ||
|
|
1251c28509 | ||
|
|
baa3dfd520 | ||
|
|
8f10a97b1e | ||
|
|
202b267bd0 | ||
|
|
d0a0e39323 | ||
|
|
bfcce19308 | ||
|
|
d24360d850 | ||
|
|
415c291345 | ||
|
|
d929590cff | ||
|
|
8291090b43 | ||
|
|
a6f77d5e0b | ||
|
|
efb082828b | ||
|
|
471e62ffab | ||
|
|
f3b55d68db | ||
|
|
4bad99e394 | ||
|
|
6fe816008f | ||
|
|
0d1f4c08f6 | ||
|
|
7dbdc1b383 | ||
|
|
51276c6cea | ||
|
|
e02897ec80 | ||
|
|
ab011cde5b | ||
|
|
a8371d354b | ||
|
|
d618892e09 |
17
.flake8
17
.flake8
@@ -1,19 +1,12 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[flake8]
|
||||
# E241: Multiple spaces after ,
|
||||
# E265: Block comment should start with '#'
|
||||
# checked by pylint:
|
||||
# F401: Unused import
|
||||
# E501: Line too long
|
||||
# F821: undefined name
|
||||
# F841: unused variable
|
||||
# E222: Multiple spaces after operator
|
||||
# F811: Redifiniton
|
||||
# W292: No newline at end of file
|
||||
# E701: multiple statements on one line
|
||||
# E702: multiple statements on one line
|
||||
# E225: missing whitespace around operator
|
||||
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702,E225
|
||||
# F401: Unused import
|
||||
# E402: module level import not at top of file
|
||||
# E266: too many leading '#' for block comment
|
||||
# W503: line break before binary operator
|
||||
ignore=E265,E501,F841,F401,E402,E266,W503
|
||||
max_complexity = 12
|
||||
exclude = ez_setup.py
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[MASTER]
|
||||
ignore=ez_setup.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=no-self-use,
|
||||
@@ -23,7 +24,13 @@ disable=no-self-use,
|
||||
too-many-instance-attributes,
|
||||
unnecessary-lambda,
|
||||
blacklisted-name,
|
||||
too-many-lines
|
||||
too-many-lines,
|
||||
logging-format-interpolation,
|
||||
interface-not-implemented,
|
||||
broad-except,
|
||||
bare-except,
|
||||
eval-used,
|
||||
exec-used
|
||||
|
||||
[BASIC]
|
||||
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
|
||||
|
||||
@@ -135,9 +135,12 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Larry Hynes
|
||||
* Johannes Altmanninger
|
||||
* Joel Torstensson
|
||||
* sbinix
|
||||
* error800
|
||||
* Thorsten Wißmann
|
||||
* Regina Hug
|
||||
* Peter Vilim
|
||||
* Patric Schmitz
|
||||
* Matthias Lisin
|
||||
* Helen Sherwood-Taylor
|
||||
// QUTE_AUTHORS_END
|
||||
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 1, 3)
|
||||
__version_info__ = (0, 1, 4)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
@@ -34,14 +34,14 @@ import faulthandler
|
||||
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
|
||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
|
||||
QStandardPaths, QObject, Qt)
|
||||
QStandardPaths, QObject, Qt, QSocketNotifier)
|
||||
|
||||
import qutebrowser
|
||||
import qutebrowser.resources # pylint: disable=unused-import
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.config import style, config, websettings
|
||||
from qutebrowser.browser import quickmarks, cookies, cache, adblock
|
||||
from qutebrowser.browser.network import qutescheme, proxy
|
||||
from qutebrowser.browser.network import qutescheme, proxy, networkmanager
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.misc import crashdialog, readline, ipc, earlyinit
|
||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||
@@ -62,6 +62,8 @@ class Application(QApplication):
|
||||
_crashdlg: The crash dialog currently open.
|
||||
_crashlogfile: A file handler to the fatal crash logfile.
|
||||
_event_filter: The EventFilter for the application.
|
||||
_signal_notifier: A QSocketNotifier used for signals on Unix.
|
||||
_signal_timer: A QTimer used to poll for signals on Windows.
|
||||
geometry: The geometry of the last closed main window.
|
||||
"""
|
||||
|
||||
@@ -145,8 +147,8 @@ class Application(QApplication):
|
||||
log.init.debug("Connecting signals...")
|
||||
self._connect_signals()
|
||||
|
||||
log.init.debug("Applying python hacks...")
|
||||
self._python_hacks()
|
||||
log.init.debug("Setting up signal handlers...")
|
||||
self._setup_signals()
|
||||
|
||||
QDesktopServices.setUrlHandler('http', self.open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('https', self.open_desktopservices_url)
|
||||
@@ -162,6 +164,8 @@ class Application(QApplication):
|
||||
|
||||
def _init_modules(self):
|
||||
"""Initialize all 'modules' which need to be initialized."""
|
||||
log.init.debug("Initializing network...")
|
||||
networkmanager.init()
|
||||
log.init.debug("Initializing readline-bridge...")
|
||||
readline_bridge = readline.ReadlineBridge()
|
||||
objreg.register('readline-bridge', readline_bridge)
|
||||
@@ -313,7 +317,7 @@ class Application(QApplication):
|
||||
win_id = self._get_window(via_ipc, force_tab=True)
|
||||
log.init.debug("Startup cmd {}".format(cmd))
|
||||
commandrunner = runners.CommandRunner(win_id)
|
||||
commandrunner.run_safely_init(cmd.lstrip(':'))
|
||||
commandrunner.run_safely_init(cmd[1:])
|
||||
elif not cmd:
|
||||
log.init.debug("Empty argument")
|
||||
win_id = self._get_window(via_ipc, force_window=True)
|
||||
@@ -376,19 +380,47 @@ class Application(QApplication):
|
||||
pass
|
||||
state_config['general']['quickstart-done'] = '1'
|
||||
|
||||
def _python_hacks(self):
|
||||
"""Get around some PyQt-oddities by evil hacks.
|
||||
def _setup_signals(self):
|
||||
"""Set up signal handlers.
|
||||
|
||||
This sets up the uncaught exception hook, quits with an appropriate
|
||||
exit status, and handles Ctrl+C properly by passing control to the
|
||||
Python interpreter once all 500ms.
|
||||
On Windows this uses a QTimer to periodically hand control over to
|
||||
Python so it can handle signals.
|
||||
|
||||
On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
|
||||
notified.
|
||||
"""
|
||||
signal.signal(signal.SIGINT, self.interrupt)
|
||||
signal.signal(signal.SIGTERM, self.interrupt)
|
||||
timer = usertypes.Timer(self, 'python_hacks')
|
||||
timer.start(500)
|
||||
timer.timeout.connect(lambda: None)
|
||||
objreg.register('python-hack-timer', timer)
|
||||
|
||||
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
|
||||
import fcntl
|
||||
read_fd, write_fd = os.pipe()
|
||||
for fd in (read_fd, write_fd):
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
self._signal_notifier = QSocketNotifier(
|
||||
read_fd, QSocketNotifier.Read, self)
|
||||
self._signal_notifier.activated.connect(self._handle_signal_wakeup)
|
||||
signal.set_wakeup_fd(write_fd)
|
||||
else:
|
||||
self._signal_timer = usertypes.Timer(self, 'python_hacks')
|
||||
self._signal_timer.start(1000)
|
||||
self._signal_timer.timeout.connect(lambda: None)
|
||||
|
||||
@pyqtSlot()
|
||||
def _handle_signal_wakeup(self):
|
||||
"""This gets called via self._signal_notifier when there's a signal.
|
||||
|
||||
Python will get control here, so the signal will get handled.
|
||||
"""
|
||||
log.destroy.debug("Handling signal wakeup!")
|
||||
self._signal_notifier.setEnabled(False)
|
||||
read_fd = self._signal_notifier.socket()
|
||||
try:
|
||||
os.read(read_fd, 1)
|
||||
except OSError:
|
||||
log.destroy.exception("Failed to read wakeup fd.")
|
||||
self._signal_notifier.setEnabled(True)
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect all signals to their slots."""
|
||||
|
||||
@@ -25,7 +25,7 @@ import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
from PyQt5.QtCore import QStandardPaths, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
@@ -108,8 +108,9 @@ class HostBlocker:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
else:
|
||||
if config.get('content', 'host-block-lists') is not None:
|
||||
message.info('last-focused',
|
||||
"Run :adblock-update to get adblock lists.")
|
||||
QTimer.singleShot(500, functools.partial(
|
||||
message.info, 'last-focused',
|
||||
"Run :adblock-update to get adblock lists."))
|
||||
|
||||
@cmdutils.register(instance='host-blocker')
|
||||
def adblock_update(self, win_id: {'special': 'win_id'}):
|
||||
|
||||
@@ -293,7 +293,11 @@ class DownloadItem(QObject):
|
||||
self.error_msg = msg
|
||||
self.stats.finish()
|
||||
self.error.emit(msg)
|
||||
self.reply.abort()
|
||||
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
|
||||
'problem, this method must only be called '
|
||||
'once.'):
|
||||
# See https://codereview.qt-project.org/#/c/107863/
|
||||
self.reply.abort()
|
||||
self.reply.deleteLater()
|
||||
self.reply = None
|
||||
self.done = True
|
||||
|
||||
@@ -24,7 +24,8 @@ import functools
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent, QClipboard
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
@@ -108,7 +109,9 @@ class HintManager(QObject):
|
||||
Signals:
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
set_open_target: Set a new target to open the links in.
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The hinting target name.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
"""
|
||||
|
||||
HINT_TEXTS = {
|
||||
@@ -129,7 +132,8 @@ class HintManager(QObject):
|
||||
}
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
set_open_target = pyqtSignal(str)
|
||||
start_hinting = pyqtSignal(str)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
"""Constructor."""
|
||||
@@ -373,20 +377,25 @@ class HintManager(QObject):
|
||||
action = "Hovering" if target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at {}/{}".format(
|
||||
action, elem, pos.x(), pos.y()))
|
||||
self.start_hinting.emit(target.name)
|
||||
if target in (Target.tab, Target.tab_bg, Target.window):
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if target != Target.hover:
|
||||
self.set_open_target.emit(target.name)
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.NoButton, Qt.NoModifier),
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, Qt.NoModifier),
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
QTimer.singleShot(0, self.stop_hinting.emit)
|
||||
|
||||
def _yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
|
||||
@@ -50,7 +50,7 @@ def parse_content_disposition(reply):
|
||||
bytes(reply.rawHeader(content_disposition_header)))
|
||||
filename = content_disposition.filename()
|
||||
except UnicodeDecodeError:
|
||||
log.misc.exception("Error while decoding filename")
|
||||
log.rfc6266.exception("Error while decoding filename")
|
||||
else:
|
||||
is_inline = content_disposition.is_inline()
|
||||
# Then try to get filename from url
|
||||
|
||||
@@ -30,7 +30,7 @@ else:
|
||||
SSL_AVAILABLE = QSslSocket.supportsSsl()
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes, utils, objreg
|
||||
from qutebrowser.utils import message, log, usertypes, utils, objreg, qtutils
|
||||
from qutebrowser.browser import cookies
|
||||
from qutebrowser.browser.network import qutescheme, networkreply
|
||||
|
||||
@@ -38,6 +38,17 @@ from qutebrowser.browser.network import qutescheme, networkreply
|
||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||
|
||||
|
||||
def init():
|
||||
"""Disable insecure SSL ciphers on old Qt versions."""
|
||||
if SSL_AVAILABLE:
|
||||
if not qtutils.version_check('5.3.0'):
|
||||
# Disable weak SSL ciphers.
|
||||
# See https://codereview.qt-project.org/#/c/75943/
|
||||
good_ciphers = [c for c in QSslSocket.supportedCiphers()
|
||||
if c.usedBits() >= 128]
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
|
||||
|
||||
class NetworkManager(QNetworkAccessManager):
|
||||
|
||||
"""Our own QNetworkAccessManager.
|
||||
|
||||
@@ -42,7 +42,7 @@ class SignalFilter(QObject):
|
||||
"""
|
||||
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
|
||||
'cur_statusbar_message']
|
||||
'cur_statusbar_message', 'cur_link_hovered']
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -32,7 +32,7 @@ from qutebrowser.config import config
|
||||
from qutebrowser.browser import http
|
||||
from qutebrowser.browser.network import networkmanager
|
||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
||||
objreg)
|
||||
objreg, debug)
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@@ -41,6 +41,9 @@ class BrowserPage(QWebPage):
|
||||
|
||||
Attributes:
|
||||
error_occured: Whether an error occured while loading.
|
||||
open_target: Where to open the next navigation request.
|
||||
("normal", "tab", "tab_bg")
|
||||
_hint_target: Override for open_target while hinting, or None.
|
||||
_extension_handlers: Mapping of QWebPage extensions to their handlers.
|
||||
_networkmnager: The NetworkManager used.
|
||||
_win_id: The window ID this BrowserPage is associated with.
|
||||
@@ -63,6 +66,8 @@ class BrowserPage(QWebPage):
|
||||
}
|
||||
self._ignore_load_started = False
|
||||
self.error_occured = False
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._hint_target = None
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id, tab_id, self)
|
||||
self.setNetworkAccessManager(self._networkmanager)
|
||||
@@ -280,6 +285,26 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
self.error_occured = False
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_start_hinting(self, hint_target):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
Args:
|
||||
hint_target: A string to set self._hint_target to.
|
||||
"""
|
||||
t = getattr(usertypes.ClickTarget, hint_target, None)
|
||||
if t is None:
|
||||
return
|
||||
log.webview.debug("Setting force target to {}/{}".format(
|
||||
hint_target, t))
|
||||
self._hint_target = t
|
||||
|
||||
@pyqtSlot()
|
||||
def on_stop_hinting(self):
|
||||
"""Emitted when hinting is finished."""
|
||||
log.webview.debug("Finishing hinting.")
|
||||
self._hint_target = None
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
ua = config.get('network', 'user-agent')
|
||||
@@ -380,17 +405,26 @@ class BrowserPage(QWebPage):
|
||||
message.error(self._win_id, "Invalid link {} clicked!".format(
|
||||
urlstr))
|
||||
log.webview.debug(url.errorString())
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
open_target = self.view().open_target
|
||||
if open_target == usertypes.ClickTarget.tab:
|
||||
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
|
||||
"target {}, open_target {}".format(
|
||||
urlstr, debug.qenum_key(QWebPage, typ),
|
||||
self._hint_target, self.open_target))
|
||||
if self._hint_target is not None:
|
||||
target = self._hint_target
|
||||
else:
|
||||
target = self.open_target
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
if target == usertypes.ClickTarget.tab:
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
elif open_target == usertypes.ClickTarget.tab_bg:
|
||||
elif target == usertypes.ClickTarget.tab_bg:
|
||||
tabbed_browser.tabopen(url, True)
|
||||
return False
|
||||
elif open_target == usertypes.ClickTarget.window:
|
||||
elif target == usertypes.ClickTarget.window:
|
||||
main_window = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
win_id = main_window.spawn()
|
||||
|
||||
@@ -55,7 +55,6 @@ class WebView(QWebView):
|
||||
statusbar_message: The current javscript statusbar message.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
load_status: loading status of this page (index into LoadStatus)
|
||||
open_target: Where to open the next tab ("normal", "tab", "tab_bg")
|
||||
viewing_source: Whether the webview is currently displaying source
|
||||
code.
|
||||
registry: The ObjectRegistry associated with this tab.
|
||||
@@ -64,7 +63,6 @@ class WebView(QWebView):
|
||||
_has_ssl_errors: Whether SSL errors occured during loading.
|
||||
_zoom: A NeighborList with the zoom levels.
|
||||
_old_scroll_pos: The old scroll position.
|
||||
_force_open_target: Override for open_target.
|
||||
_check_insertmode: If True, in mouseReleaseEvent we should check if we
|
||||
need to enter/leave insert mode.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
@@ -99,8 +97,6 @@ class WebView(QWebView):
|
||||
self.scroll_pos = (-1, -1)
|
||||
self.statusbar_message = ''
|
||||
self._old_scroll_pos = (-1, -1)
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._force_open_target = None
|
||||
self._zoom = None
|
||||
self._has_ssl_errors = False
|
||||
self.init_neighborlist()
|
||||
@@ -124,7 +120,8 @@ class WebView(QWebView):
|
||||
self.setPage(page)
|
||||
hintmanager = hints.HintManager(win_id, self.tab_id, self)
|
||||
hintmanager.mouse_event.connect(self.on_mouse_event)
|
||||
hintmanager.set_open_target.connect(self.set_force_open_target)
|
||||
hintmanager.start_hinting.connect(page.on_start_hinting)
|
||||
hintmanager.stop_hinting.connect(page.on_stop_hinting)
|
||||
objreg.register('hintmanager', hintmanager, registry=self.registry)
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
@@ -261,8 +258,8 @@ class WebView(QWebView):
|
||||
self._check_insertmode = False
|
||||
try:
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
except webelem.IsNullError:
|
||||
log.mouse.warning("Element vanished!")
|
||||
except (webelem.IsNullError, RuntimeError):
|
||||
log.mouse.warning("Element/page vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element (delayed)!")
|
||||
@@ -280,36 +277,30 @@ class WebView(QWebView):
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if self._force_open_target is not None:
|
||||
self.open_target = self._force_open_target
|
||||
self._force_open_target = None
|
||||
log.mouse.debug("Setting force target: {}".format(
|
||||
self.open_target))
|
||||
elif (e.button() == Qt.MidButton or
|
||||
e.modifiers() & Qt.ControlModifier):
|
||||
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
|
||||
background_tabs = config.get('tabs', 'background-tabs')
|
||||
if e.modifiers() & Qt.ShiftModifier:
|
||||
background_tabs = not background_tabs
|
||||
if background_tabs:
|
||||
self.open_target = usertypes.ClickTarget.tab_bg
|
||||
target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
self.open_target = usertypes.ClickTarget.tab
|
||||
log.mouse.debug("Middle click, setting target: {}".format(
|
||||
self.open_target))
|
||||
target = usertypes.ClickTarget.tab
|
||||
self.page().open_target = target
|
||||
log.mouse.debug("Middle click, setting target: {}".format(target))
|
||||
else:
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self.page().open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the webview."""
|
||||
self.shutting_down.emit()
|
||||
self.page().shutdown()
|
||||
# We disable javascript because that prevents some segfaults when
|
||||
# quitting it seems.
|
||||
log.destroy.debug("Shutting down {!r}.".format(self))
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
|
||||
self.stop()
|
||||
self.page().shutdown()
|
||||
|
||||
def openurl(self, url):
|
||||
"""Open a URL in the browser.
|
||||
@@ -435,17 +426,6 @@ class WebView(QWebView):
|
||||
"left.".format(mode))
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_force_open_target(self, target):
|
||||
"""Change the forced link target. Setter for _force_open_target.
|
||||
|
||||
Args:
|
||||
target: A string to set self._force_open_target to.
|
||||
"""
|
||||
t = getattr(usertypes.ClickTarget, target)
|
||||
log.webview.debug("Setting force target to {}/{}".format(target, t))
|
||||
self._force_open_target = t
|
||||
|
||||
def createWindow(self, wintype):
|
||||
"""Called by Qt when a page wants to create a new window.
|
||||
|
||||
@@ -504,7 +484,7 @@ class WebView(QWebView):
|
||||
|
||||
This does the following things:
|
||||
- Check if a link was clicked with the middle button or Ctrl and
|
||||
set the open_target attribute accordingly.
|
||||
set the page's open_target attribute accordingly.
|
||||
- Emit the editable_elem_selected signal if an editable element was
|
||||
clicked.
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ import configparser
|
||||
import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QStandardPaths, QUrl
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QStandardPaths, QUrl,
|
||||
QSettings)
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
@@ -164,6 +165,18 @@ def init(args):
|
||||
('completion', 'history-length'))
|
||||
objreg.register('command-history', command_history)
|
||||
|
||||
# Set the QSettings path to something like
|
||||
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
|
||||
# doesn't overwrite our config.
|
||||
#
|
||||
# This fixes one of the corruption issues here:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/515
|
||||
|
||||
config_path = standarddir.get(QStandardPaths.ConfigLocation, args)
|
||||
path = os.path.join(config_path, 'qsettings')
|
||||
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
|
||||
QSettings.setPath(fmt, QSettings.UserScope, path)
|
||||
|
||||
|
||||
class ConfigManager(QObject):
|
||||
|
||||
|
||||
@@ -569,14 +569,7 @@ DATA = collections.OrderedDict([
|
||||
|
||||
('searchengines', sect.ValueList(
|
||||
typ.SearchEngineName(), typ.SearchEngineUrl(),
|
||||
('DEFAULT', '${duckduckgo}'),
|
||||
('duckduckgo', 'https://duckduckgo.com/?q={}'),
|
||||
('ddg', '${duckduckgo}'),
|
||||
('google', 'https://encrypted.google.com/search?q={}'),
|
||||
('g', '${google}'),
|
||||
('wikipedia', 'http://en.wikipedia.org/w/index.php?'
|
||||
'title=Special:Search&search={}'),
|
||||
('wiki', '${wikipedia}'),
|
||||
('DEFAULT', 'https://duckduckgo.com/?q={}'),
|
||||
)),
|
||||
|
||||
('aliases', sect.ValueList(
|
||||
@@ -800,27 +793,27 @@ DATA = collections.OrderedDict([
|
||||
"Font used for the debugging console."),
|
||||
|
||||
('web-family-standard',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
SettingValue(typ.FontFamily(none_ok=True), ''),
|
||||
"Font family for standard fonts."),
|
||||
|
||||
('web-family-fixed',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
SettingValue(typ.FontFamily(none_ok=True), ''),
|
||||
"Font family for fixed fonts."),
|
||||
|
||||
('web-family-serif',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
SettingValue(typ.FontFamily(none_ok=True), ''),
|
||||
"Font family for serif fonts."),
|
||||
|
||||
('web-family-sans-serif',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
SettingValue(typ.FontFamily(none_ok=True), ''),
|
||||
"Font family for sans-serif fonts."),
|
||||
|
||||
('web-family-cursive',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
SettingValue(typ.FontFamily(none_ok=True), ''),
|
||||
"Font family for cursive fonts."),
|
||||
|
||||
('web-family-fantasy',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
SettingValue(typ.FontFamily(none_ok=True), ''),
|
||||
"Font family for fantasy fonts."),
|
||||
|
||||
('web-size-minimum',
|
||||
|
||||
@@ -661,9 +661,9 @@ class Font(BaseType):
|
||||
) |
|
||||
# size (<float>pt | <int>px)
|
||||
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
|
||||
)\ # size/weight/style are space-separated
|
||||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>[A-Za-z, "-]*)$ # mandatory font family""", re.VERBOSE)
|
||||
)\ # size/weight/style are space-separated
|
||||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE)
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
@@ -675,6 +675,25 @@ class Font(BaseType):
|
||||
raise configexc.ValidationError(value, "must be a valid font")
|
||||
|
||||
|
||||
class FontFamily(Font):
|
||||
|
||||
"""A Qt font family."""
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
match = self.font_regex.match(value)
|
||||
if not match:
|
||||
raise configexc.ValidationError(value, "must be a valid font")
|
||||
for group in 'style', 'weight', 'namedweight', 'size':
|
||||
if match.group(group):
|
||||
raise configexc.ValidationError(value, "may not include a "
|
||||
"{}!".format(group))
|
||||
|
||||
|
||||
class QtFont(Font):
|
||||
|
||||
"""A Font which gets converted to q QFont."""
|
||||
|
||||
@@ -22,180 +22,353 @@
|
||||
Module attributes:
|
||||
ATTRIBUTES: A mapping from internal setting names to QWebSetting enum
|
||||
constants.
|
||||
SETTERS: A mapping from setting names to QWebSetting setter method names.
|
||||
settings: The global QWebSettings singleton instance.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtCore import QStandardPaths, QUrl
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, standarddir, objreg
|
||||
from qutebrowser.utils import standarddir, objreg, log, utils, debug
|
||||
|
||||
MapType = usertypes.enum('MapType', ['attribute', 'setter', 'static_setter'])
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class Base:
|
||||
|
||||
"""Base class for QWebSetting wrappers.
|
||||
|
||||
Attributes:
|
||||
_default: The default value of this setting.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._default = UNSET
|
||||
|
||||
def _get_qws(self, qws):
|
||||
"""Get the QWebSettings object to use.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if qws is None:
|
||||
return QWebSettings.globalSettings()
|
||||
else:
|
||||
return qws
|
||||
|
||||
def save_default(self, qws=None):
|
||||
"""Save the default value based on the currently set one.
|
||||
|
||||
This does nothing if no getter is configured for this setting.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
try:
|
||||
self._default = self.get(qws)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def restore_default(self, qws=None):
|
||||
"""Restore the default value from the saved one.
|
||||
|
||||
This does nothing if the default has never been set.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if self._default is not UNSET:
|
||||
self._set(self._default, qws=qws)
|
||||
|
||||
def get(self, qws=None):
|
||||
"""Get the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set(self, value, qws=None):
|
||||
"""Set the value of this setting.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if value is None:
|
||||
self.restore_default(qws)
|
||||
else:
|
||||
self._set(value, qws=qws)
|
||||
|
||||
def _set(self, value, qws):
|
||||
"""Inner function to set the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Attribute(Base):
|
||||
|
||||
"""A setting set via QWebSettings::setAttribute.
|
||||
|
||||
Attributes:
|
||||
self._attribute: A QWebSettings::WebAttribute instance.
|
||||
"""
|
||||
|
||||
def __init__(self, attribute):
|
||||
super().__init__()
|
||||
self._attribute = attribute
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, attribute=debug.qenum_key(QWebSettings, self._attribute),
|
||||
constructor=True)
|
||||
|
||||
def get(self, qws=None):
|
||||
return self._get_qws(qws).attribute(self._attribute)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
self._get_qws(qws).setAttribute(self._attribute, value)
|
||||
|
||||
|
||||
class Setter(Base):
|
||||
|
||||
"""A setting set via QWebSettings getter/setter methods.
|
||||
|
||||
This will pass the QWebSettings instance ("self") as first argument to the
|
||||
methods, so self._getter/self._setter are the *unbound* methods.
|
||||
|
||||
Attributes:
|
||||
_getter: The unbound QWebSettings method to get this value, or None.
|
||||
_setter: The unbound QWebSettings method to set this value.
|
||||
_args: An iterable of the arguments to pass to the setter/getter
|
||||
(before the value, for the setter).
|
||||
_unpack: Whether to unpack args (True) or pass them directly (False).
|
||||
"""
|
||||
|
||||
def __init__(self, getter, setter, args=(), unpack=False):
|
||||
super().__init__()
|
||||
self._getter = getter
|
||||
self._setter = setter
|
||||
self._args = args
|
||||
self._unpack = unpack
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, getter=self._getter, setter=self._setter,
|
||||
args=self._args, unpack=self._unpack,
|
||||
constructor=True)
|
||||
|
||||
def get(self, qws=None):
|
||||
if self._getter is None:
|
||||
raise AttributeError("No getter set!")
|
||||
return self._getter(self._get_qws(qws), *self._args)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
args = [self._get_qws(qws)]
|
||||
args.extend(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
class NullStringSetter(Setter):
|
||||
|
||||
"""A setter for settings requiring a null QString as default.
|
||||
|
||||
This overrides save_default so None is saved for an empty string. This is
|
||||
needed for the CSS media type, because it returns an empty Python string
|
||||
when getting the value, but setting it to the default requires passing None
|
||||
(a null QString) instead of an empty string.
|
||||
"""
|
||||
|
||||
def save_default(self, qws=None):
|
||||
try:
|
||||
val = self.get(qws)
|
||||
except AttributeError:
|
||||
pass
|
||||
if val == '':
|
||||
self._set(None, qws=qws)
|
||||
else:
|
||||
self._set(val, qws=qws)
|
||||
|
||||
|
||||
class GlobalSetter(Setter):
|
||||
|
||||
"""A setting set via static QWebSettings getter/setter methods.
|
||||
|
||||
self._getter/self._setter are the *bound* methods.
|
||||
"""
|
||||
|
||||
def get(self, qws=None):
|
||||
if qws is not None:
|
||||
raise ValueError("qws may not be set with GlobalSetters!")
|
||||
if self._getter is None:
|
||||
raise AttributeError("No getter set!")
|
||||
return self._getter(*self._args)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
if qws is not None:
|
||||
raise ValueError("qws may not be set with GlobalSetters!")
|
||||
args = list(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
MAPPINGS = {
|
||||
'content': {
|
||||
'allow-images':
|
||||
(MapType.attribute, QWebSettings.AutoLoadImages),
|
||||
Attribute(QWebSettings.AutoLoadImages),
|
||||
'allow-javascript':
|
||||
(MapType.attribute, QWebSettings.JavascriptEnabled),
|
||||
Attribute(QWebSettings.JavascriptEnabled),
|
||||
'javascript-can-open-windows':
|
||||
(MapType.attribute, QWebSettings.JavascriptCanOpenWindows),
|
||||
Attribute(QWebSettings.JavascriptCanOpenWindows),
|
||||
'javascript-can-close-windows':
|
||||
(MapType.attribute, QWebSettings.JavascriptCanCloseWindows),
|
||||
Attribute(QWebSettings.JavascriptCanCloseWindows),
|
||||
'javascript-can-access-clipboard':
|
||||
(MapType.attribute, QWebSettings.JavascriptCanAccessClipboard),
|
||||
Attribute(QWebSettings.JavascriptCanAccessClipboard),
|
||||
#'allow-java':
|
||||
# (MapType.attribute, QWebSettings.JavaEnabled),
|
||||
# Attribute(QWebSettings.JavaEnabled),
|
||||
'allow-plugins':
|
||||
(MapType.attribute, QWebSettings.PluginsEnabled),
|
||||
Attribute(QWebSettings.PluginsEnabled),
|
||||
'local-content-can-access-remote-urls':
|
||||
(MapType.attribute, QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'local-content-can-access-file-urls':
|
||||
(MapType.attribute, QWebSettings.LocalContentCanAccessFileUrls),
|
||||
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
},
|
||||
'network': {
|
||||
'dns-prefetch':
|
||||
(MapType.attribute, QWebSettings.DnsPrefetchEnabled),
|
||||
Attribute(QWebSettings.DnsPrefetchEnabled),
|
||||
},
|
||||
'input': {
|
||||
'spatial-navigation':
|
||||
(MapType.attribute, QWebSettings.SpatialNavigationEnabled),
|
||||
Attribute(QWebSettings.SpatialNavigationEnabled),
|
||||
'links-included-in-focus-chain':
|
||||
(MapType.attribute, QWebSettings.LinksIncludedInFocusChain),
|
||||
Attribute(QWebSettings.LinksIncludedInFocusChain),
|
||||
},
|
||||
'fonts': {
|
||||
'web-family-standard':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.StandardFont, v),
|
||||
""),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.StandardFont]),
|
||||
'web-family-fixed':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.FixedFont, v),
|
||||
""),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.FixedFont]),
|
||||
'web-family-serif':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.SerifFont, v),
|
||||
""),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.SerifFont]),
|
||||
'web-family-sans-serif':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.SansSerifFont, v),
|
||||
""),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.SansSerifFont]),
|
||||
'web-family-cursive':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.CursiveFont, v),
|
||||
""),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.CursiveFont]),
|
||||
'web-family-fantasy':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.FantasyFont, v),
|
||||
""),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.FantasyFont]),
|
||||
'web-size-minimum':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.MinimumFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumFontSize]),
|
||||
'web-size-minimum-logical':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.MinimumLogicalFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumLogicalFontSize]),
|
||||
'web-size-default':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.DefaultFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFontSize]),
|
||||
'web-size-default-fixed':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.DefaultFixedFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFixedFontSize]),
|
||||
},
|
||||
'ui': {
|
||||
'zoom-text-only':
|
||||
(MapType.attribute, QWebSettings.ZoomTextOnly),
|
||||
Attribute(QWebSettings.ZoomTextOnly),
|
||||
'frame-flattening':
|
||||
(MapType.attribute, QWebSettings.FrameFlatteningEnabled),
|
||||
Attribute(QWebSettings.FrameFlatteningEnabled),
|
||||
'user-stylesheet':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setUserStyleSheetUrl(v),
|
||||
QUrl()),
|
||||
Setter(getter=QWebSettings.userStyleSheetUrl,
|
||||
setter=QWebSettings.setUserStyleSheetUrl),
|
||||
'css-media-type':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setCSSMediaType(v)),
|
||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||
setter=QWebSettings.setCSSMediaType),
|
||||
#'accelerated-compositing':
|
||||
# (MapType.attribute, QWebSettings.AcceleratedCompositingEnabled),
|
||||
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
||||
#'tiled-backing-store':
|
||||
# (MapType.attribute, QWebSettings.TiledBackingStoreEnabled),
|
||||
# Attribute(QWebSettings.TiledBackingStoreEnabled),
|
||||
},
|
||||
'storage': {
|
||||
'offline-storage-database':
|
||||
(MapType.attribute, QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
Attribute(QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
'offline-web-application-storage':
|
||||
(MapType.attribute,
|
||||
QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
'local-storage':
|
||||
(MapType.attribute, QWebSettings.LocalStorageEnabled),
|
||||
Attribute(QWebSettings.LocalStorageEnabled),
|
||||
'maximum-pages-in-cache':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setMaximumPagesInCache(v)),
|
||||
GlobalSetter(getter=QWebSettings.maximumPagesInCache,
|
||||
setter=QWebSettings.setMaximumPagesInCache),
|
||||
'object-cache-capacities':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setObjectCacheCapacities(*v)),
|
||||
GlobalSetter(getter=None,
|
||||
setter=QWebSettings.setObjectCacheCapacities,
|
||||
unpack=True),
|
||||
'offline-storage-default-quota':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setOfflineStorageDefaultQuota(v)),
|
||||
GlobalSetter(getter=QWebSettings.offlineStorageDefaultQuota,
|
||||
setter=QWebSettings.setOfflineStorageDefaultQuota),
|
||||
'offline-web-application-cache-quota':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setOfflineWebApplicationCacheQuota(v)),
|
||||
GlobalSetter(
|
||||
getter=QWebSettings.offlineWebApplicationCacheQuota,
|
||||
setter=QWebSettings.setOfflineWebApplicationCacheQuota),
|
||||
},
|
||||
'general': {
|
||||
'private-browsing':
|
||||
(MapType.attribute, QWebSettings.PrivateBrowsingEnabled),
|
||||
Attribute(QWebSettings.PrivateBrowsingEnabled),
|
||||
'developer-extras':
|
||||
(MapType.attribute, QWebSettings.DeveloperExtrasEnabled),
|
||||
Attribute(QWebSettings.DeveloperExtrasEnabled),
|
||||
'print-element-backgrounds':
|
||||
(MapType.attribute, QWebSettings.PrintElementBackgrounds),
|
||||
Attribute(QWebSettings.PrintElementBackgrounds),
|
||||
'xss-auditing':
|
||||
(MapType.attribute, QWebSettings.XSSAuditingEnabled),
|
||||
Attribute(QWebSettings.XSSAuditingEnabled),
|
||||
'site-specific-quirks':
|
||||
(MapType.attribute, QWebSettings.SiteSpecificQuirksEnabled),
|
||||
Attribute(QWebSettings.SiteSpecificQuirksEnabled),
|
||||
'default-encoding':
|
||||
(MapType.setter, lambda qws, v: qws.setDefaultTextEncoding(v), ""),
|
||||
Setter(getter=QWebSettings.defaultTextEncoding,
|
||||
setter=QWebSettings.setDefaultTextEncoding),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
settings = None
|
||||
UNSET = object()
|
||||
|
||||
|
||||
def _set_setting(typ, arg, default=UNSET, value=UNSET):
|
||||
"""Set a QWebSettings setting.
|
||||
|
||||
Args:
|
||||
typ: The type of the item.
|
||||
arg: The argument (attribute/handler)
|
||||
default: The value to use if the user set an empty string.
|
||||
value: The value to set.
|
||||
"""
|
||||
if not isinstance(typ, MapType):
|
||||
raise TypeError("Type {} is no MapType member!".format(typ))
|
||||
if value is UNSET:
|
||||
raise TypeError("No value given!")
|
||||
if value is None:
|
||||
if default is UNSET:
|
||||
return
|
||||
else:
|
||||
value = default
|
||||
|
||||
if typ == MapType.attribute:
|
||||
settings.setAttribute(arg, value)
|
||||
elif typ == MapType.setter:
|
||||
arg(settings, value)
|
||||
elif typ == MapType.static_setter:
|
||||
arg(value)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the global QWebSettings."""
|
||||
cachedir = standarddir.get(QStandardPaths.CacheLocation)
|
||||
QWebSettings.setIconDatabasePath(cachedir)
|
||||
if config.get('general', 'private-browsing'):
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(cachedir)
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(cachedir, 'application-cache'))
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation)
|
||||
@@ -204,20 +377,28 @@ def init():
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(datadir, 'offline-storage'))
|
||||
|
||||
global settings
|
||||
settings = QWebSettings.globalSettings()
|
||||
for sectname, section in MAPPINGS.items():
|
||||
for optname, mapping in section.items():
|
||||
mapping.save_default()
|
||||
value = config.get(sectname, optname)
|
||||
_set_setting(*mapping, value=value)
|
||||
log.misc.debug("Setting {} -> {} to {!r}".format(sectname, optname,
|
||||
value))
|
||||
mapping.set(value)
|
||||
objreg.get('config').changed.connect(update_settings)
|
||||
|
||||
|
||||
def update_settings(section, option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
_set_setting(*mapping, value=value)
|
||||
if (section, option) == ('general', 'private-browsing'):
|
||||
cachedir = standarddir.get(QStandardPaths.CacheLocation)
|
||||
if config.get('general', 'private-browsing'):
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(cachedir)
|
||||
else:
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
mapping.set(value)
|
||||
|
||||
@@ -26,8 +26,9 @@ Module attributes:
|
||||
import functools
|
||||
|
||||
from PyQt5.QtGui import QWindow
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QEvent
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKitWidgets import QWebView
|
||||
|
||||
from qutebrowser.keyinput import modeparsers, keyparser
|
||||
from qutebrowser.config import config
|
||||
@@ -106,15 +107,29 @@ class EventFilter(QObject):
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Forward events to the correct modeman."""
|
||||
if not self._activated:
|
||||
return False
|
||||
try:
|
||||
modeman = objreg.get('mode-manager', scope='window',
|
||||
window='current')
|
||||
return modeman.eventFilter(obj, event)
|
||||
except objreg.RegistryUnavailableError:
|
||||
# No window available yet, or not a MainWindow
|
||||
return False
|
||||
if not self._activated:
|
||||
return False
|
||||
if event.type() not in [QEvent.KeyPress, QEvent.KeyRelease]:
|
||||
# We're not interested in non-key-events so we pass them
|
||||
# through.
|
||||
return False
|
||||
if not isinstance(obj, QWindow):
|
||||
# We already handled this same event at some point earlier, so
|
||||
# we're not interested in it anymore.
|
||||
return False
|
||||
if (QApplication.instance().activeWindow() not in
|
||||
objreg.window_registry.values()):
|
||||
# Some other window (print dialog, etc.) is focused so we pass
|
||||
# the event through.
|
||||
return False
|
||||
try:
|
||||
modeman = objreg.get('mode-manager', scope='window',
|
||||
window='current')
|
||||
return modeman.eventFilter(event)
|
||||
except objreg.RegistryUnavailableError:
|
||||
# No window available yet, or not a MainWindow
|
||||
return False
|
||||
except:
|
||||
# If there is an exception in here and we leave the eventfilter
|
||||
# activated, we'll get an infinite loop and a stack overflow.
|
||||
@@ -181,9 +196,13 @@ class ModeManager(QObject):
|
||||
handled = handler(event) if handler is not None else False
|
||||
|
||||
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
|
||||
|
||||
if handled:
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (curmode in self.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
@@ -196,12 +215,11 @@ class ModeManager(QObject):
|
||||
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
|
||||
"passthrough: {}, is_non_alnum: {} --> filter: "
|
||||
"{} (focused: {!r})".format(
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
curmode in self.passthrough,
|
||||
is_non_alnum, filter_this,
|
||||
QApplication.instance().focusWidget()))
|
||||
curmode in self.passthrough, is_non_alnum,
|
||||
is_tab, filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
def _eventFilter_keyrelease(self, event):
|
||||
@@ -313,7 +331,7 @@ class ModeManager(QObject):
|
||||
self._forward_unbound_keys = config.get(
|
||||
'input', 'forward-unbound-keys')
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
def eventFilter(self, event):
|
||||
"""Filter all events based on the currently set mode.
|
||||
|
||||
Also calls the real keypress handler.
|
||||
@@ -327,21 +345,7 @@ class ModeManager(QObject):
|
||||
if self.mode is None:
|
||||
# We got events before mode is set, so just pass them through.
|
||||
return False
|
||||
typ = event.type()
|
||||
if typ not in [QEvent.KeyPress, QEvent.KeyRelease]:
|
||||
# We're not interested in non-key-events so we pass them through.
|
||||
return False
|
||||
if not isinstance(obj, QWindow):
|
||||
# We already handled this same event at some point earlier, so
|
||||
# we're not interested in it anymore.
|
||||
return False
|
||||
if (QApplication.instance().activeWindow() not in
|
||||
objreg.window_registry.values()):
|
||||
# Some other window (print dialog, etc.) is focused so we pass
|
||||
# the event through.
|
||||
return False
|
||||
|
||||
if typ == QEvent.KeyPress:
|
||||
if event.type() == QEvent.KeyPress:
|
||||
return self._eventFilter_keypress(event)
|
||||
else:
|
||||
return self._eventFilter_keyrelease(event)
|
||||
|
||||
@@ -164,7 +164,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
self.history.append(text)
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
|
||||
if text[0] in signals:
|
||||
signals[text[0]].emit(text.lstrip(text[0]))
|
||||
signals[text[0]].emit(text[1:])
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit, QSizePolicy
|
||||
|
||||
from qutebrowser.mainwindow.statusbar import textbase, prompter
|
||||
from qutebrowser.utils import objreg, utils
|
||||
@@ -35,6 +36,16 @@ class PromptLineEdit(misc.MinimalLineEditMixin, QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
QLineEdit.__init__(self, parent)
|
||||
misc.MinimalLineEditMixin.__init__(self)
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
|
||||
def sizeHint(self):
|
||||
"""Dynamically calculate the needed size."""
|
||||
height = super().sizeHint().height()
|
||||
text = self.text()
|
||||
if not text:
|
||||
text = 'x'
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
|
||||
class Prompt(QWidget):
|
||||
@@ -58,6 +69,8 @@ class Prompt(QWidget):
|
||||
self._hbox.addWidget(self.txt)
|
||||
|
||||
self.lineedit = PromptLineEdit()
|
||||
self.lineedit.setSizePolicy(QSizePolicy.MinimumExpanding,
|
||||
QSizePolicy.Fixed)
|
||||
self._hbox.addWidget(self.lineedit)
|
||||
|
||||
prompter_obj = prompter.Prompter(win_id)
|
||||
|
||||
@@ -39,6 +39,11 @@ from qutebrowser.utils import (log, message, usertypes, utils, qtutils, objreg,
|
||||
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
|
||||
|
||||
|
||||
class TabDeletedError(Exception):
|
||||
|
||||
"""Exception raised when _tab_index is called for a deleted tab."""
|
||||
|
||||
|
||||
class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
"""A TabWidget with QWebViews inside.
|
||||
@@ -59,6 +64,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tabbar -> new-tab-position set to 'left'.
|
||||
_tab_insert_idx_right: Same as above, for 'right'.
|
||||
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
||||
_shutting_down: Whether we're currently shutting down.
|
||||
|
||||
Signals:
|
||||
cur_progress: Progress of the current tab changed (loadProgress).
|
||||
@@ -97,6 +103,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._win_id = win_id
|
||||
self._tab_insert_idx_left = 0
|
||||
self._tab_insert_idx_right = -1
|
||||
self._shutting_down = False
|
||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.currentChanged.connect(self.on_current_changed)
|
||||
self.cur_load_started.connect(self.on_cur_load_started)
|
||||
@@ -118,6 +125,21 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=self.count())
|
||||
|
||||
def _tab_index(self, tab):
|
||||
"""Get the index of a given tab.
|
||||
|
||||
Raises TabDeletedError if the tab doesn't exist anymore.
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError as e:
|
||||
log.webview.debug("Got invalid tab ({})!".format(e))
|
||||
raise TabDeletedError(e)
|
||||
if idx == -1:
|
||||
log.webview.debug("Got invalid tab (index is -1)!")
|
||||
raise TabDeletedError("index is -1!")
|
||||
return idx
|
||||
|
||||
def widgets(self):
|
||||
"""Get a list of open tab widgets.
|
||||
|
||||
@@ -199,10 +221,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
def shutdown(self):
|
||||
"""Try to shut down all tabs cleanly."""
|
||||
try:
|
||||
self.currentChanged.disconnect()
|
||||
except TypeError:
|
||||
log.destroy.exception("Error while shutting down tabs")
|
||||
self._shutting_down = True
|
||||
for tab in self.widgets():
|
||||
self._remove_tab(tab)
|
||||
|
||||
@@ -417,14 +436,10 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab: The tab where the signal belongs to.
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if idx == -1:
|
||||
# We can get signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
self.setTabIcon(idx, QIcon())
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -449,16 +464,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
log.webview.debug("Ignoring title change to '{}'.".format(text))
|
||||
return
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
log.webview.debug("Changing title for idx {} to '{}'".format(
|
||||
idx, text))
|
||||
if idx == -1:
|
||||
# We can get signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
self.setTabText(idx, text.replace('&', '&&'))
|
||||
if idx == self.currentIndex():
|
||||
self._change_app_title(text)
|
||||
@@ -472,14 +483,10 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
url: The new URL.
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if idx == -1:
|
||||
# We can get signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
if not self.tabText(idx):
|
||||
self.setTabText(idx, url)
|
||||
|
||||
@@ -495,14 +502,10 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if not config.get('tabs', 'show-favicons'):
|
||||
return
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if idx == -1:
|
||||
# We can get *_changed signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
self.setTabIcon(idx, tab.icon())
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -520,8 +523,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
@pyqtSlot(int)
|
||||
def on_current_changed(self, idx):
|
||||
"""Set last-focused-tab and leave hinting mode when focus changed."""
|
||||
if idx == -1:
|
||||
# closing the last tab (before quitting)
|
||||
if idx == -1 or self._shutting_down:
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||
@@ -545,8 +548,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
def on_load_progress(self, tab, perc):
|
||||
"""Adjust tab indicator on load progress."""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
start = config.get('colors', 'tabs.indicator.start')
|
||||
@@ -563,8 +566,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if tab.page().error_occured:
|
||||
|
||||
@@ -21,16 +21,14 @@
|
||||
|
||||
import sys
|
||||
import code
|
||||
import rlcompleter
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QStringListModel
|
||||
from PyQt5.QtWidgets import (QTextEdit, QWidget, QVBoxLayout, QApplication,
|
||||
QCompleter)
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import cmdhistory, miscwidgets
|
||||
from qutebrowser.utils import utils, log, objreg
|
||||
from qutebrowser.utils import utils, objreg
|
||||
|
||||
|
||||
class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
@@ -39,8 +37,6 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
|
||||
Attributes:
|
||||
_history: The command history of executed commands.
|
||||
_rlcompleter: The rlcompleter.Completer instance.
|
||||
_qcompleter: The QCompleter instance.
|
||||
|
||||
Signals:
|
||||
execute: Emitted when a commandline should be executed.
|
||||
@@ -48,46 +44,18 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
|
||||
execute = pyqtSignal(str)
|
||||
|
||||
def __init__(self, namespace, parent):
|
||||
def __init__(self, _namespace, parent):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
namespace: The local namespace of the interpreter.
|
||||
_namespace: The local namespace of the interpreter.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.update_font()
|
||||
objreg.get('config').changed.connect(self.update_font)
|
||||
self.textChanged.connect(self.on_text_changed)
|
||||
|
||||
self._rlcompleter = rlcompleter.Completer(namespace)
|
||||
qcompleter = QCompleter(self)
|
||||
self._model = QStringListModel(qcompleter)
|
||||
qcompleter.setModel(self._model)
|
||||
qcompleter.setCompletionMode(
|
||||
QCompleter.UnfilteredPopupCompletion)
|
||||
qcompleter.setModelSorting(
|
||||
QCompleter.CaseSensitivelySortedModel)
|
||||
self.setCompleter(qcompleter)
|
||||
|
||||
self._history = cmdhistory.History()
|
||||
self.returnPressed.connect(self.on_return_pressed)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_text_changed(self, text):
|
||||
"""Update completion when text changed."""
|
||||
strings = set()
|
||||
i = 0
|
||||
while True:
|
||||
s = self._rlcompleter.complete(text, i)
|
||||
if s is None:
|
||||
break
|
||||
else:
|
||||
strings.add(s)
|
||||
i += 1
|
||||
strings = sorted(list(strings))
|
||||
self._model.setStringList(strings)
|
||||
log.misc.vdebug('completions: {!r}'.format(strings))
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_return_pressed(self):
|
||||
"""Execute the line of code which was entered."""
|
||||
|
||||
@@ -52,7 +52,7 @@ def parse_fatal_stacktrace(text):
|
||||
lines = [
|
||||
r'Fatal Python error: (.*)',
|
||||
r' *',
|
||||
r'Current thread [^ ]* \(most recent call first\): *',
|
||||
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *',
|
||||
r' File ".*", line \d+ in (.*)',
|
||||
]
|
||||
m = re.match('\n'.join(lines), text)
|
||||
@@ -60,7 +60,7 @@ def parse_fatal_stacktrace(text):
|
||||
# We got some invalid text.
|
||||
return ('', '')
|
||||
else:
|
||||
return (m.group(1), m.group(2))
|
||||
return (m.group(1), m.group(3))
|
||||
|
||||
|
||||
def get_fatal_crash_dialog(debug, data):
|
||||
|
||||
@@ -1095,6 +1095,9 @@ class FontTests(unittest.TestCase):
|
||||
# (style, weight, pointsize, pixelsize, family
|
||||
'"Foobar Neue"':
|
||||
FontDesc(QFont.StyleNormal, QFont.Normal, -1, -1, 'Foobar Neue'),
|
||||
'inconsolatazi4':
|
||||
FontDesc(QFont.StyleNormal, QFont.Normal, -1, -1,
|
||||
'inconsolatazi4'),
|
||||
'10pt "Foobar Neue"':
|
||||
FontDesc(QFont.StyleNormal, QFont.Normal, 10, None, 'Foobar Neue'),
|
||||
'10PT "Foobar Neue"':
|
||||
@@ -1199,6 +1202,59 @@ class FontTests(unittest.TestCase):
|
||||
self.assertIsNone(self.t2.transform(''))
|
||||
|
||||
|
||||
class FontFamilyTests(unittest.TestCase):
|
||||
|
||||
"""Test FontFamily."""
|
||||
|
||||
TESTS = ['"Foobar Neue"', 'inconsolatazi4', 'Foobar']
|
||||
INVALID = [
|
||||
'10pt "Foobar Neue"',
|
||||
'10PT "Foobar Neue"',
|
||||
'10px "Foobar Neue"',
|
||||
'10PX "Foobar Neue"',
|
||||
'bold "Foobar Neue"',
|
||||
'italic "Foobar Neue"',
|
||||
'oblique "Foobar Neue"',
|
||||
'normal bold "Foobar Neue"',
|
||||
'bold italic "Foobar Neue"',
|
||||
'bold 10pt "Foobar Neue"',
|
||||
'italic 10pt "Foobar Neue"',
|
||||
'oblique 10pt "Foobar Neue"',
|
||||
'normal bold 10pt "Foobar Neue"',
|
||||
'bold italic 10pt "Foobar Neue"',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.t = configtypes.FontFamily()
|
||||
|
||||
def test_validate_empty(self):
|
||||
"""Test validate with an empty string."""
|
||||
with self.assertRaises(configexc.ValidationError):
|
||||
self.t.validate('')
|
||||
|
||||
def test_validate_empty_none_ok(self):
|
||||
"""Test validate with an empty string and none_ok=True."""
|
||||
t = configtypes.FontFamily(none_ok=True)
|
||||
t.validate('')
|
||||
|
||||
def test_validate_valid(self):
|
||||
"""Test validate with valid values."""
|
||||
for val in self.TESTS:
|
||||
with self.subTest(val=val):
|
||||
self.t.validate(val)
|
||||
|
||||
def test_validate_invalid(self):
|
||||
"""Test validate with invalid values."""
|
||||
for val in self.INVALID:
|
||||
with self.subTest(val=val):
|
||||
with self.assertRaises(configexc.ValidationError, msg=val):
|
||||
self.t.validate(val)
|
||||
|
||||
def test_transform_empty(self):
|
||||
"""Test transform with an empty value."""
|
||||
self.assertIsNone(self.t.transform(''))
|
||||
|
||||
|
||||
class RegexTests(unittest.TestCase):
|
||||
|
||||
"""Test Regex."""
|
||||
|
||||
@@ -40,6 +40,13 @@ Current thread 0x00007f09b538d700 (most recent call first):
|
||||
File "filename", line 88 in func
|
||||
"""
|
||||
|
||||
VALID_CRASH_TEXT_THREAD = """
|
||||
Fatal Python error: Segmentation fault
|
||||
_
|
||||
Thread 0x00007fa135ac7700 (most recent call first):
|
||||
File "", line 1 in testfunc
|
||||
"""
|
||||
|
||||
INVALID_CRASH_TEXT = """
|
||||
Hello world!
|
||||
"""
|
||||
@@ -56,7 +63,14 @@ class ParseFatalStacktraceTests(unittest.TestCase):
|
||||
self.assertEqual(typ, "Segmentation fault")
|
||||
self.assertEqual(func, 'testfunc')
|
||||
|
||||
def test_valid_text(self):
|
||||
def test_valid_text_thread(self):
|
||||
"""Test parse_fatal_stacktrace with a valid text #2."""
|
||||
text = VALID_CRASH_TEXT_THREAD.strip().replace('_', ' ')
|
||||
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
||||
self.assertEqual(typ, "Segmentation fault")
|
||||
self.assertEqual(func, 'testfunc')
|
||||
|
||||
def test_valid_text_empty(self):
|
||||
"""Test parse_fatal_stacktrace with a valid text but empty function."""
|
||||
text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ')
|
||||
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
||||
|
||||
@@ -28,6 +28,8 @@ import sys
|
||||
|
||||
from qutebrowser.utils import log
|
||||
|
||||
from PyQt5.QtCore import qWarning
|
||||
|
||||
|
||||
class BaseTest(unittest.TestCase):
|
||||
|
||||
@@ -208,5 +210,32 @@ class InitLogTests(BaseTest):
|
||||
log.init_log(self.args)
|
||||
sys.stderr = old_stderr
|
||||
|
||||
|
||||
class HideQtWarningTests(BaseTest):
|
||||
|
||||
"""Tests for hide_qt_warning/QtWarningFilter."""
|
||||
|
||||
def test_unfiltered(self):
|
||||
"""Test a message which is not filtered."""
|
||||
with log.hide_qt_warning("World", logger='qt-tests'):
|
||||
with self.assertLogs('qt-tests', logging.WARNING):
|
||||
qWarning("Hello World")
|
||||
|
||||
def test_filtered_exact(self):
|
||||
"""Test a message which is filtered (exact match)."""
|
||||
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
||||
qWarning("Hello")
|
||||
|
||||
def test_filtered_start(self):
|
||||
"""Test a message which is filtered (match at line start)."""
|
||||
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
||||
qWarning("Hello World")
|
||||
|
||||
def test_filtered_whitespace(self):
|
||||
"""Test a message which is filtered (match with whitespace)."""
|
||||
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
||||
qWarning(" Hello World ")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -166,6 +166,7 @@ class IsUrlTests(unittest.TestCase):
|
||||
'deadbeef',
|
||||
'31c3',
|
||||
'http:foo:0',
|
||||
'foo::bar',
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -298,6 +298,36 @@ def qt_message_handler(msg_type, context, msg):
|
||||
qt.handle(record)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def hide_qt_warning(pattern, logger='qt'):
|
||||
"""Hide Qt warnings matching the given regex."""
|
||||
log_filter = QtWarningFilter(pattern)
|
||||
logger_obj = logging.getLogger(logger)
|
||||
logger_obj.addFilter(log_filter)
|
||||
yield
|
||||
logger_obj.removeFilter(log_filter)
|
||||
|
||||
|
||||
class QtWarningFilter(logging.Filter):
|
||||
|
||||
"""Filter to filter Qt warnings.
|
||||
|
||||
Attributes:
|
||||
_pattern: The start of the message.
|
||||
"""
|
||||
|
||||
def __init__(self, pattern):
|
||||
super().__init__()
|
||||
self._pattern = pattern
|
||||
|
||||
def filter(self, record):
|
||||
"""Determine if the specified record is to be logged."""
|
||||
if record.msg.strip().startswith(self._pattern):
|
||||
return False # filter
|
||||
else:
|
||||
return True # log
|
||||
|
||||
|
||||
class LogFilter(logging.Filter):
|
||||
|
||||
"""Filter to filter log records based on the commandline argument.
|
||||
|
||||
@@ -192,7 +192,14 @@ def _has_explicit_scheme(url):
|
||||
Args:
|
||||
url: The URL as QUrl.
|
||||
"""
|
||||
return url.isValid() and url.scheme() and not url.path().startswith(' ')
|
||||
|
||||
# Note that generic URI syntax actually would allow a second colon
|
||||
# after the scheme delimiter. Since we don't know of any URIs
|
||||
# using this and want to support e.g. searching for scoped C++
|
||||
# symbols, we treat this as not an URI anyways.
|
||||
return (url.isValid() and url.scheme()
|
||||
and not url.path().startswith(' ')
|
||||
and not url.path().startswith(':'))
|
||||
|
||||
|
||||
def is_special_url(url):
|
||||
|
||||
@@ -388,13 +388,15 @@ def fake_io(write_func):
|
||||
fake_stdout = FakeIOStream(write_func)
|
||||
sys.stderr = fake_stderr
|
||||
sys.stdout = fake_stdout
|
||||
yield
|
||||
# If the code we did run did change sys.stdout/sys.stderr, we leave it
|
||||
# unchanged. Otherwise, we reset it.
|
||||
if sys.stdout is fake_stdout:
|
||||
sys.stdout = old_stdout
|
||||
if sys.stderr is fake_stderr:
|
||||
sys.stderr = old_stderr
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# If the code we did run did change sys.stdout/sys.stderr, we leave it
|
||||
# unchanged. Otherwise, we reset it.
|
||||
if sys.stdout is fake_stdout:
|
||||
sys.stdout = old_stdout
|
||||
if sys.stderr is fake_stderr:
|
||||
sys.stderr = old_stderr
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
||||
@@ -315,8 +315,11 @@ def generate_settings(filename):
|
||||
|
||||
def _get_authors():
|
||||
"""Get a list of authors based on git commit logs."""
|
||||
corrections = {'binix': 'sbinix'}
|
||||
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
|
||||
cnt = collections.Counter(commits.decode('utf-8').splitlines())
|
||||
authors = [corrections.get(author, author)
|
||||
for author in commits.decode('utf-8').splitlines()]
|
||||
cnt = collections.Counter(authors)
|
||||
return sorted(cnt, key=lambda k: (cnt[k], k), reverse=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user