Compare commits

...

41 Commits

Author SHA1 Message Date
Florian Bruhin
130d2cb33b Release v0.1.4 2015-03-19 06:39:20 +01:00
Florian Bruhin
d36a0d5d15 Regenerate authors. 2015-03-19 06:38:42 +01:00
Florian Bruhin
a126924c42 Fix lint. 2015-03-19 06:21:08 +01:00
Florian Bruhin
a0381e1683 Make it possible to correct author names in src2asciidoc. 2015-03-19 06:21:08 +01:00
Florian Bruhin
891bb86175 Update icon db path when private-browsing changed. 2015-03-19 06:21:08 +01:00
binix
d9f356652f Stop the icon database from being created when private-browsing is set to true 2015-03-19 06:21:08 +01:00
Florian Bruhin
a4a6099515 Don't poll for signals on Unix.
A better solution is to use QSocketNotifier and os.wakeup_fd to get notified
about new signals.

Thanks to Yuya Nishihara / TortoiseHG for the hint!

Fixes #555.
2015-03-19 06:21:08 +01:00
Florian Bruhin
44dd4da33f Discard uninteresting events early in eventFilter.
Before, we ran quite a lot of code (e.g. objreg) on every event, even if it
turns out to not be a keypress/release event at all.
2015-03-19 06:21:08 +01:00
Florian Bruhin
f69470ddcd Ensure there's no size for font-family settings.
See #549.
2015-03-19 06:21:08 +01:00
Florian Bruhin
71dbdb37a2 Refactor websettings and save/restore defaults.
This makes qutebrowser.config.websettings much easier to understand, and saves
all defaults so it can restore them properly when a setting is set to an empty
string.

Before, when we set the fonts to empty strings instead of the true default, in
some cases anti-aliasing was broken.

Fixes #549.
2015-03-19 06:21:08 +01:00
error800
6c0e470b60 Removed default search engines. Closes #533. 2015-03-19 06:21:08 +01:00
Florian Bruhin
2322ee4f2c Add an unittest for foo::bar URLs.
See #544, #546.
2015-03-19 06:21:08 +01:00
Patric Schmitz
669760ed8f Handle URLs with double-colon at the beginning as search strings
Closes #544. We might also merge #542 now.
2015-03-19 06:21:08 +01:00
Florian Bruhin
2b34fbc073 Don't try to add tab repr in TabDeletedError.
This will always fail with another RuntimeError...
2015-03-19 06:21:08 +01:00
Florian Bruhin
e8b689ab50 Adjust prompt size hint based on content.
See #26.
Fixes #506.

Related to 06cc982ab5.
2015-03-19 06:21:08 +01:00
Florian Bruhin
20c3e8dd52 Ignore RuntimeError in mouserelease_insertmode.
It seems when clicking certain elements, the webview can get deleted before the
singleShot QTimer will activate.
2015-03-19 06:21:08 +01:00
Florian Bruhin
4ec618386b Hide Qt warning when aborting download reply. 2015-03-19 06:21:08 +01:00
Florian Bruhin
05f5083c9c log.utils: Add Qt warning filter context manager. 2015-03-19 06:21:08 +01:00
Florian Bruhin
1251c28509 Use _shutting_down instead of disconnecting signal
This will most likely cause less pain than disconnecting the signal, which
seems to be broken on OS X.
2015-03-18 23:13:43 +01:00
Florian Bruhin
baa3dfd520 Hide "Error while shutting down tabs" message.
This makes no sense at all, yet seems to happen when closing qutebrowser on OS
X via Cmd+Q.
2015-03-18 23:13:43 +01:00
Florian Bruhin
8f10a97b1e Clear open target in acceptNavigationRequest.
This is a regression introduced in a76868c0f4.
Fixes #530.
2015-03-18 23:13:43 +01:00
Florian Bruhin
202b267bd0 Fix handling of signals with deleted tabs. 2015-03-18 23:13:43 +01:00
Florian Bruhin
d0a0e39323 Don't log cur_link_hovered signals. 2015-03-18 23:13:43 +01:00
Florian Bruhin
bfcce19308 Add logging for acceptNavigationRequest. 2015-03-18 23:13:43 +01:00
Florian Bruhin
d24360d850 Log rfc6266 UnicodeDecodeError to correct logger. 2015-03-18 23:13:43 +01:00
Florian Bruhin
415c291345 Fix AttributeError when doing extended hinting. 2015-03-18 23:13:43 +01:00
Florian Bruhin
d929590cff Refactor how click/hint open targets are handled. 2015-03-18 23:13:43 +01:00
Florian Bruhin
8291090b43 Remove debug console completing completely.
Turns out pylint doesn't like it if stuff is unused because we commented code
out ;)
2015-03-18 23:13:43 +01:00
Florian Bruhin
a6f77d5e0b Restore sys.std* in utils.fake_io on exceptions. 2015-03-18 23:13:42 +01:00
Florian Bruhin
efb082828b Reset open_target in acceptNavigationRequest.
After ddb39275eb, when something was opened via
hints in a new tab, the open_target still was set afterwards and the next
regular open did open in a new tab.
2015-03-18 23:13:42 +01:00
Florian Bruhin
471e62ffab hints: Include button in buttons().
From the QMouseEvent::buttons documentation:

    For mouse move events, this is all buttons that are pressed down. For mouse
    press and double click events this includes the button that caused the
    event. For mouse release events this excludes the button that caused the
    event.
2015-03-18 23:13:42 +01:00
Florian Bruhin
f3b55d68db Simulate Ctrl-click when hinting in new tab/win.
This works around the fact some pages (e.g. github) load their content via AJAX
on a normal left click, so we'll never get acceptNavigationRequest and thus
can't open them in a new tab.

Fixes #488.
2015-03-18 23:13:42 +01:00
Florian Bruhin
4bad99e394 Allow font names with integers in them. 2015-03-18 23:13:42 +01:00
Florian Bruhin
6fe816008f Disable insecure SSL ciphers (< 128bit) for Qt 5.2.
This is only an issue for the users which are stuck on Ubuntu Trusty.
2015-03-18 23:13:42 +01:00
Florian Bruhin
0d1f4c08f6 Fix QIODevice warnings when closing tabs.
This is a regression introduced in 43c9d69295.
Fixes #517.
2015-03-18 22:23:36 +01:00
Florian Bruhin
7dbdc1b383 Fix wrong parsing of faulthandler logs. 2015-03-18 22:22:49 +01:00
Florian Bruhin
51276c6cea Improve parsing of faulthandler logs. 2015-03-18 22:22:01 +01:00
Florian Bruhin
e02897ec80 Set the QSettings path to a config-subdirectory.
QWebInspector uses QSettings to save its GUI-settings. However, the default
path for QSettings is ~/.config/qutebrowser/qutebrowser.conf which overwrites
our own config file.

This fixes one part of #515.
2015-03-18 22:20:53 +01:00
Florian Bruhin
ab011cde5b Add workaround for adblock-message without window. 2015-03-18 22:19:25 +01:00
Florian Bruhin
a8371d354b Fix searching for terms starting with a slash.
Fixes #507.
2015-03-18 22:18:11 +01:00
Florian Bruhin
d618892e09 Ignore tab key presses if they'd switch focus.
If the mainwindow is focused but not the web view (e.g. in prompt mode), an
unbound tab key should be filtered so it doesn't change keyboard focus.

Fixes #504.
2015-03-18 22:17:17 +01:00
31 changed files with 732 additions and 322 deletions

17
.flake8
View File

@@ -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

View File

@@ -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_]*(__)?$

View File

@@ -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

View File

@@ -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."

View File

@@ -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."""

View File

@@ -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'}):

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

View File

@@ -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()

View File

@@ -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.

View File

@@ -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):

View File

@@ -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',

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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()

View File

@@ -166,6 +166,7 @@ class IsUrlTests(unittest.TestCase):
'deadbeef',
'31c3',
'http:foo:0',
'foo::bar',
)
def setUp(self):

View File

@@ -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.

View File

@@ -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):

View File

@@ -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

View File

@@ -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)