Compare commits

...

39 Commits

Author SHA1 Message Date
Florian Bruhin
a90a894177 Release v1.0.4 2017-11-28 10:51:29 +01:00
Florian Bruhin
ce305b0fed Make pylint shut up
This is fixed in master, but it makes no sense to backport that.
2017-11-28 08:21:36 +01:00
Florian Bruhin
b19b8163e1 Handle sqlite errors during :history-clear
(cherry picked from commit ef1825efb0)
2017-11-28 07:05:23 +01:00
Florian Bruhin
203664a12c Add SQLITE_CORRUPT to environmental SQL errors
(cherry picked from commit 73587b1e16)
2017-11-28 07:05:21 +01:00
Florian Bruhin
cfe7e7883a Handle "out of memory" error in sql.init()
(cherry picked from commit 4fed8518e1)
2017-11-28 07:05:15 +01:00
Florian Bruhin
dfe74ff0ee Handle empty messages in qt_message_handler
I can't reproduce this, but someone on KDE reported always getting a crash (as
msg.splitlines()[0] gives an IndexError) when trying to select a file with
Qt 5.9.3.

(cherry picked from commit 67253726fa)
2017-11-26 17:32:05 +01:00
Florian Bruhin
18a45bbd5b Break long lines 2017-11-24 16:03:21 +01:00
Florian Bruhin
30462b3839 Fix overflow handling for QtWebKit scrolling
If we do "m * val / 100", the value gets bigger, so we need to check for an
overflow afterwards.
2017-11-24 16:02:50 +01:00
Florian Bruhin
1a7669c8a2 Ignore new Qt 5.9.3 error message
(cherry picked from commit 187554a1673553e907185f4ba146e717fc68b2ae)
2017-11-24 14:23:24 +01:00
Florian Bruhin
da5f8269c7 Update to PyQt 5.9.2
(cherry picked from commit 9174676deb777def6998f84f447f59ea7b968786)
2017-11-24 14:23:02 +01:00
Florian Bruhin
39353cdc00 Fix content.cache.size overflow with QtWebEngine
While 64-bit values are allowed with QtWebKit/QNetworkDiskCache, QtWebEngine
only allows 32-bit values here. With the updated sip's strict overflow checking,
that means we get an exception when setting a too big value.

(cherry picked from commit 203b6c354f)
2017-11-21 14:12:20 +01:00
Florian Bruhin
6edfd1801d Handle OSError in :config-write-py
(cherry picked from commit 4d0511597b01c3f445b877cbb47e655c7dd5434c)
2017-11-17 09:09:16 +01:00
Florian Bruhin
21364f135e Improve documentation for :hint --rapid
(cherry picked from commit 456c854f06)
2017-11-10 13:19:39 +01:00
cryzed
6f6eeae902 Ignore additional Qt error messages
(cherry picked from commit 222c51aa6e)
2017-11-10 09:42:53 +01:00
cryzed
c551922e2d Fix issue #3251
(cherry picked from commit 6e719d1796)
2017-11-10 09:42:51 +01:00
Florian Bruhin
f7585eb60e Fix qute://gpl
(cherry picked from commit 378b280f9a)
2017-11-06 12:14:44 +01:00
Florian Bruhin
fc63cea917 Release v1.0.3 2017-11-04 16:48:53 +01:00
Florian Bruhin
effbe36919 Also update sip version 2017-11-04 16:35:21 +01:00
Florian Bruhin
db466097e0 Update requirements to PyQt 5.9.1
This will break things as there's no PyQt 5.9.1 Linux wheel available yet, but
it'll make it possible to release a new qutebrowser release for macOS and
Windows with Qt 5.9.2 at least.
2017-11-04 16:10:47 +01:00
Florian Bruhin
0446babb3d Update changelog from master 2017-11-04 16:10:30 +01:00
Florian Bruhin
85d643ff49 Fix unbinding default keys twice
When doing :unbind with a default keybinding the first time, it gets inserted
into bindings.commands with None as value.

When then doing :unbind a second time, instead of just leaving that None value
as-is, we removed it again (because it got treated as a custom binding).

Fixes #3162

(cherry picked from commit 5689a3c0dc)
2017-11-04 15:22:16 +01:00
Ryan Roden-Corrent
12c35ff836 Don't check date string in test_histcategory.
We really just need to check that the row exists here, the date doesn't
matter. Checking the date here is actually flaky with regards to time.
When running locally at 11:50 EST, it failed with:

```
assert self._model.data(self._model.index(row, col)) == item
AssertionError: assert '1969-12-31' == '1970-01-01'
- 1969-12-31
+ 1970-01-01
```

It was wrong to assume that an atime of 0 would always format to
1970-01-01.

(cherry picked from commit a9926e44f0)
2017-11-04 15:21:57 +01:00
Ryan Roden-Corrent
b33bb94593 Ensure completions are sorted after filtering.
I previously removed the sorting logic from SortFilter thinking it was
unnecessary if we construct the model with a sorted list. However, this
only worked when no pattern was set, and the items are misordered as
soon as a pattern is input.

This patch reintroduces alpha-sorting, which can be disabled by passing
sort=False to the ListCategory constructor. The session completion test
had to be tweaked as it simulated the incorrect assumption that the
session list is not alpha-ordered; sessions come out of the
session-manager pre-sorted so we may as well use alpha-sorting in the
session completion model.

Resolves #3156.

(cherry picked from commit 47447c047a)
2017-11-04 15:21:54 +01:00
Florian Bruhin
25a11f492b Un-hide :open-editor
It can be used in normal mode as well, and it's nice to have it discoverable.
Fixes #3235.

(cherry picked from commit bb208f4e77)
2017-11-04 15:21:27 +01:00
Florian Bruhin
d0453542c3 Clarify qute://configdiff/old title
(cherry picked from commit 1c39715267)
2017-11-04 15:18:26 +01:00
Florian Bruhin
4bff63f17b Use lts version of NopeJS
Looks like npm doesn't work with Node v9:
https://github.com/nodejs/node/issues/16649

(cherry picked from commit 385337eb90)
2017-11-02 11:28:24 +01:00
Jay Kamat
a5426ca4ac Abort pinned tab prompt if tab is destroyed
Closes #3223

(cherry picked from commit cb7e6ab02d)
2017-11-02 09:16:03 +01:00
Jay Kamat
ec9981f0d1 Fix a couple style issues
(cherry picked from commit 64b6852ae3)
2017-10-31 07:06:55 +01:00
Jay Kamat
5ebca1c309 Fix ellipsis on pinned tabs with index > 10
See #3209

(cherry picked from commit 2a4163b2c7)
2017-10-31 07:06:55 +01:00
Jay Kamat
1559965e6b Add a simple benchmark for _update_tab_titles
(cherry picked from commit 97d719b179)
2017-10-31 07:06:55 +01:00
Jay Kamat
9a2d451bc5 Lower tabbar cache bound and clean up code
(cherry picked from commit cb6f4313d7)
2017-10-31 07:06:55 +01:00
Jay Kamat
5a080ebea5 Add most recent tab bar to cache statistics
(cherry picked from commit 49daa7aab8)
2017-10-31 07:06:55 +01:00
Jay Kamat
2f3e3c3073 Prevent calling _tab_pinned on every tab twice
(cherry picked from commit b499474599)
2017-10-31 07:06:55 +01:00
Jay Kamat
457bba7e79 Fix blowing cache for different icons
(cherry picked from commit caae1c7008)
2017-10-31 07:06:55 +01:00
Jay Kamat
0045af8738 Clear cache on config changes
(cherry picked from commit fde4495bc7)
2017-10-31 07:06:55 +01:00
Jay Kamat
7dc82407e8 Rename _minimum_tab_size_hint_helper
(cherry picked from commit e705ea7e56)
2017-10-31 07:06:55 +01:00
Jay Kamat
0becdfa6b0 Add caching for tab sizes
(cherry picked from commit 08b562ea0c)
2017-10-31 07:06:36 +01:00
Florian Bruhin
b69b5c81fc Remove -f for :bind in configuring.asciidoc
[ci skip]

(cherry picked from commit 17e0f6d23c)
2017-10-27 07:17:57 +02:00
Florian Bruhin
8607ddce24 Fix error code for "database is locked"
See #2930

(cherry picked from commit 96bec9f9d7)
2017-10-17 15:36:12 +02:00
41 changed files with 310 additions and 100 deletions

View File

@@ -51,7 +51,7 @@ matrix:
env: TESTENV=eslint
language: node_js
python: null
node_js: node
node_js: "lts/*"
fast_finish: true
cache:

View File

@@ -109,7 +109,7 @@ The following software and libraries are required to run qutebrowser:
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
supported
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.9 recommended) for Python 3
(5.9.2 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]

View File

@@ -15,10 +15,45 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v1.0.4
------
Fixed
~~~~~
- The `qute://gpl` page now works correctly again.
- Trying to bind an empty command now doesn't crash anymore.
- Fixed crash when `:config-write-py` fails to write to the given path.
- Fixed crash for some users when selecting a file with Qt 5.9.3
- Improved handling for various SQL errors
- Fix crash when setting content.cache.size to a big value (> 2 GB)
v1.0.3
------
Changed
~~~~~~~
- macOS and Windows builds are now built with PyQt 5.9.1 and Qt 5.9.2, including
various bugfixes, as well as security fixes from Chromium up to version
61.0.3163.79.
- Performance improvements for tab rendering.
- The :open-editor command is now not hidden anymore as it's also usable in
normal mode.
Fixed
~~~~~
- Handle accessing a locked sqlite database gracefully
- Abort pinned tab dialogs properly when a tab is closed e.g. by closing a
window
- Unbinding a default keybinding twice now doesn't bind it again
- Completions are now sorted correctly again when filtered
v1.0.2
------
Fixes
Fixed
~~~~~
- Fix workaround for black screens or crashes with Nvidia cards
@@ -36,7 +71,7 @@ Changed
v1.0.1
------
Fixes
Fixed
~~~~~
- Fixed starting after customizing `fonts.tabs` or `fonts.debug_console`.
@@ -74,6 +109,9 @@ Major changes
the entire browsing history. The default for
`completion.web_history_max_items` got changed to `-1` (unlimited). If the
completion is too slow on your machine, try setting it to a few 1000 items.
- Up/Down now navigates through the command history instead of selecting
completion items. Either use Tab to cycle through the completion, or
https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#migrating-older-configurations[restore the old behavior].
Added
~~~~~

View File

@@ -60,6 +60,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<messages,messages>>|Show a log of past messages.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<print,print>>|Print the current/[count]th tab.
|<<quickmark-add,quickmark-add>>|Add a new quickmark.
|<<quickmark-del,quickmark-del>>|Delete a quickmark.
@@ -485,7 +486,9 @@ Start hinting.
==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily
open multiple links. This is only possible with targets
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
* +*-m*+, +*--mode*+: The hinting mode to use.
@@ -653,6 +656,12 @@ The tab index to open the URL in.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `editor.command` config option.
[[print]]
=== print
Syntax: +:print [*--preview*] [*--pdf* 'file']+
@@ -1137,7 +1146,6 @@ How many steps to zoom out.
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<nop,nop>>|Do nothing.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
@@ -1388,12 +1396,6 @@ How many blocks to move.
=== nop
Do nothing.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `editor.command` config option.
[[prompt-accept]]
=== prompt-accept
Syntax: +:prompt-accept ['value']+

View File

@@ -22,8 +22,8 @@ Other changes in default settings:
doing:
+
----
:bind -f -m command <Up> completion-item-focus prev
:bind -f -m command <Down> completion-item-focus next
:bind -m command <Up> completion-item-focus prev
:bind -m command <Down> completion-item-focus next
----
- The default for `completion.web_history_max_items` is now set to `-1`, showing

View File

@@ -1407,6 +1407,7 @@ This setting is only available with the QtWebKit backend.
[[content.cache.size]]
=== content.cache.size
Size of the HTTP network cache. Null to use the default value.
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
Type: <<types,Int>>

View File

@@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.9
sip==4.19.3
PyQt5==5.9.2
sip==4.19.6

View File

@@ -1 +1 @@
PyQt5
PyQt5

View File

@@ -55,4 +55,6 @@ qt_log_ignore =
^QQuickWidget::invalidateRenderControl could not make context current
^libpng warning: iCCP: known incorrect sRGB profile
^inotify_add_watch(".*") failed: "No space left on device"
^QSettings::value: Empty key passed
^Icon theme ".*" not found
xfail_strict = true

View File

@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (1, 0, 2)
__version_info__ = (1, 0, 4)
__version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@@ -1599,8 +1599,7 @@ class CommandDispatcher:
self.on_editing_finished, elem))
ed.edit(text)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.register(instance='command-dispatcher', scope='window')
def open_editor(self):
"""Open an external editor with the currently selected form field.

View File

@@ -616,8 +616,10 @@ class HintManager(QObject):
"""Start hinting.
Args:
rapid: Whether to do rapid hinting. This is only possible with
targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
rapid: Whether to do rapid hinting. With rapid hinting, the hint
mode isn't left after a hint is followed, so you can easily
open multiple links. This is only possible with targets
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
add_history: Whether to add the spawned or yanked link to the
browsing history.

View File

@@ -21,6 +21,7 @@
import os
import time
import contextlib
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer
@@ -87,6 +88,16 @@ class WebHistory(sql.SqlTable):
def __contains__(self, url):
return self._contains_query.run(val=url).value()
@contextlib.contextmanager
def _handle_sql_errors(self):
try:
yield
except sql.SqlError as e:
if e.environmental:
message.error("Failed to write history: {}".format(e.text()))
else:
raise
def _rebuild_completion(self):
data = {'url': [], 'title': [], 'last_atime': []}
# select the latest entry for each url
@@ -142,8 +153,9 @@ class WebHistory(sql.SqlTable):
"history?")
def _do_clear(self):
self.delete_all()
self.completion.delete_all()
with self._handle_sql_errors():
self.delete_all()
self.completion.delete_all()
def delete_url(self, url):
"""Remove all history entries with the given url.
@@ -191,7 +203,7 @@ class WebHistory(sql.SqlTable):
atime = int(atime) if (atime is not None) else int(time.time())
try:
with self._handle_sql_errors():
self.insert({'url': self._format_url(url),
'title': title,
'atime': atime,
@@ -202,11 +214,6 @@ class WebHistory(sql.SqlTable):
'title': title,
'last_atime': atime
}, replace=True)
except sql.SqlError as e:
if e.environmental:
message.error("Failed to write history: {}".format(e.text()))
else:
raise
def _parse_entry(self, line):
"""Parse a history line like '12345 http://example.com title'."""

View File

@@ -307,7 +307,7 @@ def qute_log(url):
@add_handler('gpl')
def qute_gpl(_url):
"""Handler for qute://gpl. Return HTML content as string."""
return 'text/html', utils.read_file('html/LICENSE.html')
return 'text/html', utils.read_file('html/license.html')
@add_handler('help')

View File

@@ -92,9 +92,10 @@ class DefaultProfileSetter(websettings.Base):
"""A setting set on the QWebEngineProfile."""
def __init__(self, setter, default=websettings.UNSET):
def __init__(self, setter, converter=None, default=websettings.UNSET):
super().__init__(default)
self._setter = setter
self._converter = converter
def __repr__(self):
return utils.get_repr(self, setter=self._setter, constructor=True)
@@ -103,7 +104,11 @@ class DefaultProfileSetter(websettings.Base):
if settings is not None:
raise ValueError("'settings' may not be set with "
"DefaultProfileSetters!")
setter = getattr(default_profile, self._setter)
if self._converter is not None:
value = self._converter(value)
setter(value)
@@ -281,7 +286,9 @@ MAPPINGS = {
Attribute(QWebEngineSettings.LocalStorageEnabled),
'content.cache.size':
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
DefaultProfileSetter('setHttpCacheMaximumSize', default=0,
converter=lambda val:
qtutils.check_overflow(val, 'int', fatal=False)),
'content.xss_auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'content.default_encoding':

View File

@@ -422,12 +422,13 @@ class WebKitScroller(browsertab.AbstractScroller):
else:
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
if val is not None:
val = qtutils.check_overflow(val, 'int', fatal=False)
frame = self._widget.page().mainFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
maximum = frame.scrollBarMaximum(orientation)
if maximum == 0:
continue
frame.setScrollBarValue(orientation, int(m * val / 100))
pos = int(maximum * val / 100)
pos = qtutils.check_overflow(pos, 'int', fatal=False)
frame.setScrollBarValue(orientation, pos)
def _key_press(self, key, count=1, getter_name=None, direction=None):
frame = self._widget.page().mainFrame()

View File

@@ -29,7 +29,7 @@ def option(*, info):
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Options", sorted(options)))
model.add_category(listcategory.ListCategory("Options", options))
return model
@@ -39,7 +39,7 @@ def customized_option(*, info):
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt, _value in info.config)
model.add_category(listcategory.ListCategory("Customized options",
sorted(options)))
options))
return model
@@ -66,8 +66,7 @@ def value(optname, *_values, info):
vals = opt.typ.complete()
if vals is not None:
model.add_category(listcategory.ListCategory("Completions",
sorted(vals)))
model.add_category(listcategory.ListCategory("Completions", vals))
return model

View File

@@ -31,7 +31,7 @@ class ListCategory(QSortFilterProxyModel):
"""Expose a list of items as a category for the CompletionModel."""
def __init__(self, name, items, delete_func=None, parent=None):
def __init__(self, name, items, sort=True, delete_func=None, parent=None):
super().__init__(parent)
self.name = name
self.srcmodel = QStandardItemModel(parent=self)
@@ -43,6 +43,7 @@ class ListCategory(QSortFilterProxyModel):
self.srcmodel.appendRow([QStandardItem(x) for x in item])
self.setSourceModel(self.srcmodel)
self.delete_func = delete_func
self._sort = sort
def set_pattern(self, val):
"""Setter for pattern.
@@ -60,19 +61,33 @@ class ListCategory(QSortFilterProxyModel):
sortcol = 0
self.sort(sortcol)
def lessThan(self, _lindex, rindex):
def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
Prefers all items which start with self._pattern. Other than that, keep
items in their original order.
Prefers all items which start with self._pattern. Other than that, uses
normal Python string sorting.
Args:
_lindex: The QModelIndex of the left item (*left* < right)
lindex: The QModelIndex of the left item (*left* < right)
rindex: The QModelIndex of the right item (left < *right*)
Return:
True if left < right, else False
"""
qtutils.ensure_valid(lindex)
qtutils.ensure_valid(rindex)
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex)
return not right.startswith(self._pattern)
leftstart = left.startswith(self._pattern)
rightstart = right.startswith(self._pattern)
if leftstart and not rightstart:
return True
elif rightstart and not leftstart:
return False
elif self._sort:
return left < right
else:
return False

View File

@@ -43,7 +43,7 @@ def helptopic(*, info):
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_category(listcategory.ListCategory("Settings", sorted(settings)))
model.add_category(listcategory.ListCategory("Settings", settings))
return model
@@ -59,7 +59,8 @@ def quickmark(*, info=None): # pylint: disable=unused-argument
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
marks = objreg.get('quickmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Quickmarks', marks,
delete_func=delete))
delete_func=delete,
sort=False))
return model
@@ -75,7 +76,8 @@ def bookmark(*, info=None): # pylint: disable=unused-argument
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
marks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Bookmarks', marks,
delete_func=delete))
delete_func=delete,
sort=False))
return model

View File

@@ -61,9 +61,9 @@ def url(*, info):
bookmarks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory(
'Quickmarks', quickmarks, delete_func=_delete_quickmark))
'Quickmarks', quickmarks, delete_func=_delete_quickmark, sort=False))
model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark))
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
if info.config.get('completion.web_history_max_items') != 0:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)

View File

@@ -170,6 +170,11 @@ class KeyConfig:
def bind(self, key, command, *, mode, save_yaml=False):
"""Add a new binding from key to command."""
if command is not None and not command.strip():
raise configexc.KeybindingError(
"Can't add binding '{}' with empty command in {} "
'mode'.format(key, mode))
key = self._prepare(key, mode)
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
key, command, mode))
@@ -198,7 +203,7 @@ class KeyConfig:
bindings_commands = self._config.get_obj('bindings.commands')
if key in val.bindings.commands[mode]:
if val.bindings.commands[mode].get(key, None) is not None:
# In custom bindings -> remove it
del bindings_commands[mode][key]
elif key in val.bindings.default[mode]:

View File

@@ -287,4 +287,7 @@ class ConfigCommands:
writer = configfiles.ConfigPyWriter(options, bindings,
commented=commented)
writer.write(filename)
try:
writer.write(filename)
except OSError as e:
raise cmdexc.CommandError(str(e))

View File

@@ -181,7 +181,10 @@ content.cache.size:
none_ok: true
minval: 0
maxval: maxint64
desc: Size of the HTTP network cache. Null to use the default value.
desc: >-
Size of the HTTP network cache. Null to use the default value.
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
# Defaults from QWebSettings::QWebSettings() in
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp

View File

@@ -755,6 +755,6 @@ def get_diff():
lexer = pygments.lexers.DiffLexer()
formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table',
title='Config diff')
title='Diffing pre-1.0 default config with pre-1.0 modified config')
# pylint: enable=no-member
return pygments.highlight(conf_diff + key_diff, lexer, formatter)

View File

@@ -249,13 +249,14 @@ class TabbedBrowser(tabwidget.TabWidget):
def tab_close_prompt_if_pinned(self, tab, force, yes_action):
"""Helper method for tab_close.
If tab is pinned, prompt. If everything is good, run yes_action.
If tab is pinned, prompt. If not, run yes_action.
If tab is destroyed, abort question.
"""
if tab.data.pinned and not force:
message.confirm_async(
title='Pinned Tab',
text="Are you sure you want to close a pinned tab?",
yes_action=yes_action, default=False)
yes_action=yes_action, default=False, abort_on=[tab.destroyed])
else:
yes_action()

View File

@@ -310,7 +310,7 @@ class TabBar(QTabBar):
return self.parent().currentWidget()
@pyqtSlot(str)
def _on_config_changed(self, option):
def _on_config_changed(self, option: str):
if option == 'fonts.tabs':
self._set_font()
elif option == 'tabs.favicons.scale':
@@ -325,6 +325,12 @@ class TabBar(QTabBar):
if option.startswith('colors.tabs.'):
self.update()
# Clear _minimum_tab_size_hint_helper cache when appropriate
if option in ["tabs.indicator_padding",
"tabs.padding",
"tabs.width.indicator"]:
self._minimum_tab_size_hint_helper.cache_clear()
def _on_show_switching_delay_changed(self):
"""Set timer interval when tabs.show_switching_delay got changed."""
self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay)
@@ -424,7 +430,7 @@ class TabBar(QTabBar):
return
super().mousePressEvent(e)
def minimumTabSizeHint(self, index, ellipsis: bool = True):
def minimumTabSizeHint(self, index, ellipsis: bool = True) -> QSize:
"""Set the minimum tab size to indicator/icon/... text.
Args:
@@ -434,38 +440,47 @@ class TabBar(QTabBar):
Return:
A QSize of the smallest tab size we can make.
"""
text = '\u2026' if ellipsis else self.tabText(index)
icon = self.tabIcon(index)
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None, self)
if icon.isNull():
icon_width = 0
else:
icon_width = icon.actualSize(QSize(extent, extent)).width()
return self._minimum_tab_size_hint_helper(self.tabText(index),
icon_width,
ellipsis)
@functools.lru_cache(maxsize=2**9)
def _minimum_tab_size_hint_helper(self, tab_text: str,
icon_width: int,
ellipsis: bool) -> QSize:
"""Helper function to cache tab results.
Config values accessed in here should be added to _on_config_changed to
ensure cache is flushed when needed.
"""
text = '\u2026' if ellipsis else tab_text
# Don't ever shorten if text is shorter than the ellipsis
text_width = min(self.fontMetrics().width(text),
self.fontMetrics().width(self.tabText(index)))
icon = self.tabIcon(index)
self.fontMetrics().width(tab_text))
padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding
padding_h = padding.left + padding.right
padding_h += indicator_padding.left + indicator_padding.right
padding_v = padding.top + padding.bottom
if icon.isNull():
icon_size = QSize(0, 0)
else:
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None,
self)
icon_size = icon.actualSize(QSize(extent, extent))
height = self.fontMetrics().height() + padding_v
width = (text_width + icon_size.width() +
width = (text_width + icon_width +
padding_h + config.val.tabs.width.indicator)
return QSize(width, height)
def _tab_total_width_pinned(self):
"""Get the current total width of pinned tabs.
This width is calculated assuming no shortening due to ellipsis."""
return sum(self.minimumTabSizeHint(idx, ellipsis=False).width()
for idx in range(self.count())
if self._tab_pinned(idx))
def _pinnedCount(self) -> int:
"""Get the number of pinned tabs."""
return sum(self._tab_pinned(idx) for idx in range(self.count()))
def _pinned_statistics(self) -> (int, int):
"""Get the number of pinned tabs and the total width of pinned tabs."""
pinned_list = [idx for idx in range(self.count())
if self._tab_pinned(idx)]
pinned_count = len(pinned_list)
pinned_width = sum(self.minimumTabSizeHint(idx, ellipsis=False).width()
for idx in pinned_list)
return (pinned_count, pinned_width)
def _tab_pinned(self, index: int) -> bool:
"""Return True if tab is pinned."""
@@ -504,8 +519,8 @@ class TabBar(QTabBar):
return QSize()
else:
pinned = self._tab_pinned(index)
no_pinned_count = self.count() - self._pinnedCount()
pinned_width = self._tab_total_width_pinned()
pinned_count, pinned_width = self._pinned_statistics()
no_pinned_count = self.count() - pinned_count
no_pinned_width = self.width() - pinned_width
if pinned:

View File

@@ -68,11 +68,23 @@ class SqliteError(SqlError):
# https://github.com/qutebrowser/qutebrowser/issues/2930
# https://github.com/qutebrowser/qutebrowser/issues/3004
environmental_errors = [
'5', # SQLITE_BUSY ("database is locked")
'8', # SQLITE_READONLY
'9', # SQLITE_LOCKED,
'13', # SQLITE_FULL,
'11', # SQLITE_CORRUPT
'13', # SQLITE_FULL
]
self.environmental = error.nativeErrorCode() in environmental_errors
# At least in init(), we can get errors like this:
# type: ConnectionError
# database text: out of memory
# driver text: Error opening database
# error code: -1
environmental_strings = [
"out of memory",
]
errcode = error.nativeErrorCode()
self.environmental = (
errcode in environmental_errors or
(errcode == -1 and error.databaseText() in environmental_strings))
def text(self):
return self.error.databaseText()

View File

@@ -171,6 +171,7 @@ def debug_cache_stats():
prefix_info = configdata.is_valid_prefix.cache_info()
# pylint: disable=protected-access
render_stylesheet_info = config._render_stylesheet.cache_info()
# pylint: enable=protected-access
history_info = None
try:
@@ -181,9 +182,17 @@ def debug_cache_stats():
except ImportError:
pass
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
# pylint: disable=protected-access
tab_bar = tabbed_browser.tabBar()
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
# pylint: enable=protected-access
log.misc.debug('is_valid_prefix: {}'.format(prefix_info))
log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info))
log.misc.debug('history: {}'.format(history_info))
log.misc.debug('tab width cache: {}'.format(tabbed_browser_info))
@cmdutils.register(debug=True)

View File

@@ -327,7 +327,8 @@ def change_console_formatter(level):
console_handler.setFormatter(console_formatter)
def qt_message_handler(msg_type, context, msg):
def qt_message_handler(msg_type, context, # pylint: disable=too-many-branches
msg):
"""Qt message handler to redirect qWarning etc. to the logging system.
Args:
@@ -421,6 +422,9 @@ def qt_message_handler(msg_type, context, msg):
'with: -9805', # flake8: disable=E131
]
if not msg:
msg = "Logged empty message!"
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
level = logging.DEBUG
else:

View File

@@ -209,3 +209,9 @@ Feature: Special qute:// pages
Scenario: Open qute://version
When I open qute://version
Then the page should contain the plaintext "Version info"
# qute://gpl
Scenario: Open qute://gpl
When I open qute://gpl
Then the page should contain the plaintext "GNU GENERAL PUBLIC LICENSE"

View File

@@ -141,6 +141,12 @@ def is_ignored_chromium_message(line):
# channel message
'Invalid node channel message',
# Qt 5.9.3
# [30217:30229:1124/141512.682110:ERROR:
# cert_verify_proc_openssl.cc(212)]
# X509 Verification error self signed certificate : 18 : 0 : 4
'X509 Verification error self signed certificate : 18 : 0 : 4',
# Not reproducible anymore?
'Running without the SUID sandbox! *',

View File

@@ -147,7 +147,7 @@ def test_remove_rows(hist, model_validator):
cat.set_pattern('')
hist.delete('url', 'foo')
cat.removeRows(0, 1)
model_validator.validate([('bar', 'Bar', '1970-01-01')])
model_validator.validate([('bar', 'Bar')])
def test_remove_rows_fetch(hist):

View File

@@ -24,27 +24,36 @@ import pytest
from qutebrowser.completion.models import listcategory
@pytest.mark.parametrize('pattern, before, after', [
@pytest.mark.parametrize('pattern, before, after, after_nosort', [
('foo',
[('foo', ''), ('bar', '')],
[('foo', '')],
[('foo', '')]),
('foo',
[('foob', ''), ('fooc', ''), ('fooa', '')],
[('fooa', ''), ('foob', ''), ('fooc', '')],
[('foob', ''), ('fooc', ''), ('fooa', '')]),
# prefer foobar as it starts with the pattern
('foo',
[('barfoo', ''), ('foobar', '')],
[('foobar', ''), ('barfoo', '')]),
[('barfoo', ''), ('foobaz', ''), ('foobar', '')],
[('foobar', ''), ('foobaz', ''), ('barfoo', '')],
[('foobaz', ''), ('foobar', ''), ('barfoo', '')]),
('foo',
[('foo', 'bar'), ('bar', 'foo'), ('bar', 'bar')],
[('foo', 'bar'), ('bar', 'foo')],
[('foo', 'bar'), ('bar', 'foo')]),
])
def test_set_pattern(pattern, before, after, model_validator):
def test_set_pattern(pattern, before, after, after_nosort, model_validator):
"""Validate the filtering and sorting results of set_pattern."""
cat = listcategory.ListCategory('Foo', before)
model_validator.set_model(cat)
cat.set_pattern(pattern)
model_validator.validate(after)
cat = listcategory.ListCategory('Foo', before, sort=False)
model_validator.set_model(cat)
cat.set_pattern(pattern)
model_validator.validate(after_nosort)

View File

@@ -466,9 +466,9 @@ def test_session_completion(qtmodeltester, session_manager_stub):
qtmodeltester.check(model)
_check_completions(model, {
"Sessions": [('default', None, None),
('1', None, None),
('2', None, None)]
"Sessions": [('1', None, None),
('2', None, None),
('default', None, None)]
})

View File

@@ -269,6 +269,28 @@ class TestKeyConfig:
match="Can't find binding 'foobar' in normal mode"):
key_config_stub.unbind('foobar', mode='normal')
def test_unbound_twice(self, key_config_stub, config_stub, no_bindings):
"""Try unbinding an already-unbound default key.
For custom-bound keys (in bindings.commands), it's okay to display an
error, as this isn't something you'd do in e.g a config.py anyways.
https://github.com/qutebrowser/qutebrowser/issues/3162
"""
config_stub.val.bindings.default = {'normal': {'a': 'nop'}}
config_stub.val.bindings.commands = no_bindings
key_config_stub.unbind('a')
assert key_config_stub.get_command('a', mode='normal') is None
key_config_stub.unbind('a')
assert key_config_stub.get_command('a', mode='normal') is None
def test_empty_command(self, key_config_stub):
"""Try binding a key to an empty command."""
message = "Can't add binding 'x' with empty command in normal mode"
with pytest.raises(configexc.KeybindingError, match=message):
key_config_stub.bind('x', ' ', mode='normal')
class TestConfig:

View File

@@ -406,6 +406,11 @@ class TestWritePy:
lines = confpy.read_text('utf-8').splitlines()
assert '# Autogenerated config.py' in lines
def test_oserror(self, commands, tmpdir):
"""Test writing to a directory which does not exist."""
with pytest.raises(cmdexc.CommandError):
commands.config_write_py(str(tmpdir / 'foo' / 'config.py'))
class TestBind:

View File

@@ -52,3 +52,16 @@ class TestTabWidget:
with qtbot.waitExposed(widget):
widget.show()
def test_update_tab_titles_benchmark(self, benchmark, widget,
qtbot, fake_web_tab):
"""Benchmark for update_tab_titles."""
widget.addTab(fake_web_tab(), 'foobar')
widget.addTab(fake_web_tab(), 'foobar2')
widget.addTab(fake_web_tab(), 'foobar3')
widget.addTab(fake_web_tab(), 'foobar4')
with qtbot.waitExposed(widget):
widget.show()
benchmark(widget._update_tab_titles)

View File

@@ -39,7 +39,7 @@ def test_sqlerror():
class TestSqliteError:
@pytest.mark.parametrize('error_code, environmental', [
('9', True), # SQLITE_LOCKED
('5', True), # SQLITE_BUSY
('19', False), # SQLITE_CONSTRAINT
])
def test_environmental(self, error_code, environmental):

View File

@@ -25,12 +25,15 @@ import itertools
import sys
import warnings
import attr
import pytest
import pytest_catchlog
from qutebrowser.utils import log
from qutebrowser.misc import utilcmds
from PyQt5 import QtCore
@pytest.fixture(autouse=True)
def restore_loggers():
@@ -252,3 +255,22 @@ def test_ignore_py_warnings(caplog):
assert len(caplog.records) == 1
msg = caplog.records[0].message.splitlines()[0]
assert msg.endswith("UserWarning: not hidden")
class TestQtMessageHandler:
@attr.s
class Context:
"""Fake QMessageLogContext."""
function = attr.ib(default=None)
category = attr.ib(default=None)
file = attr.ib(default=None)
line = attr.ib(default=None)
def test_empty_message(self, caplog):
"""Make sure there's no crash with an empty message."""
log.qt_message_handler(QtCore.QtDebugMsg, self.Context(), "")
assert len(caplog.records) == 1
assert caplog.records[0].msg == "Logged empty message!"

View File

@@ -86,7 +86,7 @@ setenv =
passenv = {[testenv]passenv}
deps =
{[testenv]deps}
PyQt5==5.9
PyQt5==5.9.2
commands = {envpython} -bb -m pytest {posargs:tests}
[testenv:py36-pyqt59]
@@ -97,7 +97,7 @@ setenv =
passenv = {[testenv]passenv}
deps =
{[testenv]deps}
PyQt5==5.9
PyQt5==5.9.2
commands = {envpython} -bb -m pytest {posargs:tests}
# test envs with coverage
@@ -110,7 +110,7 @@ setenv =
passenv = {[testenv]passenv}
deps =
{[testenv]deps}
PyQt5==5.9
PyQt5==5.9.2
commands =
{envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
{envpython} scripts/dev/check_coverage.py {posargs}
@@ -123,7 +123,7 @@ setenv =
passenv = {[testenv]passenv}
deps =
{[testenv]deps}
PyQt5==5.9
PyQt5==5.9.2
commands =
{envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
{envpython} scripts/dev/check_coverage.py {posargs}