Merge branch 'master' of https://github.com/qutebrowser/qutebrowser into jay/visible-update-titles

This commit is contained in:
Jay Kamat 2018-09-29 12:29:50 -07:00
commit 322b053cbf
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
17 changed files with 323 additions and 75 deletions

View File

@ -43,6 +43,10 @@ Added
- Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only - Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only
once), as support for those is going to be removed in a future release. once), as support for those is going to be removed in a future release.
- New t[iI][hHu] default bindings (similar to `tsh` etc.) to toggle images. - New t[iI][hHu] default bindings (similar to `tsh` etc.) to toggle images.
- New `tabs.max_width` setting which allows to have a more "normal" look for
tabs.
- New `content.mute` setting which allows to mute pages (or all tabs) by
default.
Changed Changed
~~~~~~~ ~~~~~~~
@ -73,6 +77,11 @@ Changed
- Editing text in an external editor now simulates a JS "input" event, which - Editing text in an external editor now simulates a JS "input" event, which
improves compatibility with websites reacting via JS to input. improves compatibility with websites reacting via JS to input.
- The `qute://settings` page is now properly sorted on Python 3.5. - The `qute://settings` page is now properly sorted on Python 3.5.
- `:zoom`, `:zoom-in` and `:zoom-out` now have a `--quiet` switch which causes
them to not display a message.
- The `scrolling.bar` setting now takes three values instead of being a
boolean: `always`, `never`, and `when-searching` (which only displays it
while a search is active).
Fixed Fixed
~~~~~ ~~~~~

View File

@ -1413,7 +1413,7 @@ Yank something to the clipboard or primary selection.
[[zoom]] [[zoom]]
=== zoom === zoom
Syntax: +:zoom ['zoom']+ Syntax: +:zoom [*--quiet*] ['zoom']+
Set the zoom level for the current tab. Set the zoom level for the current tab.
@ -1422,20 +1422,33 @@ The zoom can be given as argument or as [count]. If neither is given, the zoom i
==== positional arguments ==== positional arguments
* +'zoom'+: The zoom percentage to set. * +'zoom'+: The zoom percentage to set.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count ==== count
The zoom percentage to set. The zoom percentage to set.
[[zoom-in]] [[zoom-in]]
=== zoom-in === zoom-in
Syntax: +:zoom-in [*--quiet*]+
Increase the zoom level for the current tab. Increase the zoom level for the current tab.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count ==== count
How many steps to zoom in. How many steps to zoom in.
[[zoom-out]] [[zoom-out]]
=== zoom-out === zoom-out
Syntax: +:zoom-out [*--quiet*]+
Decrease the zoom level for the current tab. Decrease the zoom level for the current tab.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count ==== count
How many steps to zoom out. How many steps to zoom out.

View File

@ -145,6 +145,7 @@
|<<content.local_storage,content.local_storage>>|Enable support for HTML 5 local storage and Web SQL. |<<content.local_storage,content.local_storage>>|Enable support for HTML 5 local storage and Web SQL.
|<<content.media_capture,content.media_capture>>|Allow websites to record audio/video. |<<content.media_capture,content.media_capture>>|Allow websites to record audio/video.
|<<content.mouse_lock,content.mouse_lock>>|Allow websites to lock your mouse pointer. |<<content.mouse_lock,content.mouse_lock>>|Allow websites to lock your mouse pointer.
|<<content.mute,content.mute>>|Automatically mute tabs.
|<<content.netrc_file,content.netrc_file>>|Netrc-file for HTTP authentication. |<<content.netrc_file,content.netrc_file>>|Netrc-file for HTTP authentication.
|<<content.notifications,content.notifications>>|Allow websites to show notifications. |<<content.notifications,content.notifications>>|Allow websites to show notifications.
|<<content.pdfjs,content.pdfjs>>|Allow pdf.js to view PDF files in the browser. |<<content.pdfjs,content.pdfjs>>|Allow pdf.js to view PDF files in the browser.
@ -231,7 +232,7 @@
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling. |<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode. |<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode.
|<<qt.process_model,qt.process_model>>|Which Chromium process model to use. |<<qt.process_model,qt.process_model>>|Which Chromium process model to use.
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar. |<<scrolling.bar,scrolling.bar>>|When to show the scrollbar.
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages. |<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
|<<search.ignore_case,search.ignore_case>>|When to find text on a page case-insensitively. |<<search.ignore_case,search.ignore_case>>|When to find text on a page case-insensitively.
|<<search.incremental,search.incremental>>|Find text on a page incrementally, renewing the search for each typed character. |<<search.incremental,search.incremental>>|Find text on a page incrementally, renewing the search for each typed character.
@ -250,6 +251,7 @@
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators. |<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators.
|<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable). |<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed. |<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|<<tabs.max_width,tabs.max_width>>|Maximum width (in pixels) of tabs (-1 for no maximum).
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior). |<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied. |<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel. |<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
@ -1915,6 +1917,17 @@ On QtWebEngine, this setting requires Qt 5.8 or newer.
On QtWebKit, this setting is unavailable. On QtWebKit, this setting is unavailable.
[[content.mute]]
=== content.mute
Automatically mute tabs.
Note that if the `:tab-mute` command is used, the mute status for the affected tab is now controlled manually, and this setting doesn't have any effect.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
[[content.netrc_file]] [[content.netrc_file]]
=== content.netrc_file === content.netrc_file
Netrc-file for HTTP authentication. Netrc-file for HTTP authentication.
@ -2815,11 +2828,17 @@ This setting is only available with the QtWebEngine backend.
[[scrolling.bar]] [[scrolling.bar]]
=== scrolling.bar === scrolling.bar
Show a scrollbar. When to show the scrollbar.
Type: <<types,Bool>> Type: <<types,String>>
Default: +pass:[false]+ Valid values:
* +always+: Always show the scrollbar.
* +never+: Never show the scrollbar.
* +when-searching+: Show the scrollbar when searching for text in the webpage. With the QtWebKit backend, this is equal to `never`.
Default: +pass:[when-searching]+
[[scrolling.smooth]] [[scrolling.smooth]]
=== scrolling.smooth === scrolling.smooth
@ -3086,6 +3105,17 @@ Valid values:
Default: +pass:[ignore]+ Default: +pass:[ignore]+
[[tabs.max_width]]
=== tabs.max_width
Maximum width (in pixels) of tabs (-1 for no maximum).
This setting only applies when tabs are horizontal.
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
This setting may not apply properly if max_width is smaller than the minimum size of tab contents, or smaller than tabs.min_width.
Type: <<types,Int>>
Default: +pass:[-1]+
[[tabs.min_width]] [[tabs.min_width]]
=== tabs.min_width === tabs.min_width
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior). Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).

View File

@ -248,10 +248,19 @@ class AbstractSearch(QObject):
this view. this view.
_flags: The flags of the last search (needs to be set by subclasses). _flags: The flags of the last search (needs to be set by subclasses).
_widget: The underlying WebView widget. _widget: The underlying WebView widget.
Signals:
finished: Emitted when a search was finished.
arg: True if the text was found, False otherwise.
cleared: Emitted when an existing search was cleared.
""" """
def __init__(self, parent=None): finished = pyqtSignal(bool)
cleared = pyqtSignal()
def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(parent)
self._tab = tab
self._widget = None self._widget = None
self.text = None self.text = None
self.search_displayed = False self.search_displayed = False
@ -668,20 +677,27 @@ class AbstractAudio(QObject):
muted_changed = pyqtSignal(bool) muted_changed = pyqtSignal(bool)
recently_audible_changed = pyqtSignal(bool) recently_audible_changed = pyqtSignal(bool)
def __init__(self, parent=None): def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(parent)
self._widget = None self._widget = None
self._tab = tab
def set_muted(self, muted: bool): def set_muted(self, muted: bool, override: bool = False):
"""Set this tab as muted or not.""" """Set this tab as muted or not.
Arguments:
override: If set to True, muting/unmuting was done manually and
overrides future automatic mute/unmute changes based on
the URL.
"""
raise NotImplementedError raise NotImplementedError
def is_muted(self): def is_muted(self):
"""Whether this tab is muted.""" """Whether this tab is muted."""
raise NotImplementedError raise NotImplementedError
def toggle_muted(self): def toggle_muted(self, *, override: bool = False):
self.set_muted(not self.is_muted()) self.set_muted(not self.is_muted(), override=override)
def is_recently_audible(self): def is_recently_audible(self):
"""Whether this tab has had audio playing recently.""" """Whether this tab has had audio playing recently."""

View File

@ -870,37 +870,41 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def zoom_in(self, count=1): def zoom_in(self, count=1, quiet=False):
"""Increase the zoom level for the current tab. """Increase the zoom level for the current tab.
Args: Args:
count: How many steps to zoom in. count: How many steps to zoom in.
quiet: Don't show a zoom level message.
""" """
tab = self._current_widget() tab = self._current_widget()
try: try:
perc = tab.zoom.offset(count) perc = tab.zoom.offset(count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
if not quiet:
message.info("Zoom level: {}%".format(int(perc)), replace=True) message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def zoom_out(self, count=1): def zoom_out(self, count=1, quiet=False):
"""Decrease the zoom level for the current tab. """Decrease the zoom level for the current tab.
Args: Args:
count: How many steps to zoom out. count: How many steps to zoom out.
quiet: Don't show a zoom level message.
""" """
tab = self._current_widget() tab = self._current_widget()
try: try:
perc = tab.zoom.offset(-count) perc = tab.zoom.offset(-count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
if not quiet:
message.info("Zoom level: {}%".format(int(perc)), replace=True) message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def zoom(self, zoom=None, count=None): def zoom(self, zoom=None, count=None, quiet=False):
"""Set the zoom level for the current tab. """Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither is The zoom can be given as argument or as [count]. If neither is
@ -910,6 +914,7 @@ class CommandDispatcher:
Args: Args:
zoom: The zoom percentage to set. zoom: The zoom percentage to set.
count: The zoom percentage to set. count: The zoom percentage to set.
quiet: Don't show a zoom level message.
""" """
if zoom is not None: if zoom is not None:
try: try:
@ -927,6 +932,7 @@ class CommandDispatcher:
tab.zoom.set_factor(float(level) / 100) tab.zoom.set_factor(float(level) / 100)
except ValueError: except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level)) raise cmdexc.CommandError("Can't zoom {}%!".format(level))
if not quiet:
message.info("Zoom level: {}%".format(int(level)), replace=True) message.info("Zoom level: {}%".format(int(level)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@ -2231,6 +2237,6 @@ class CommandDispatcher:
if tab is None: if tab is None:
return return
try: try:
tab.audio.toggle_muted() tab.audio.toggle_muted(override=True)
except browsertab.WebTabError as e: except browsertab.WebTabError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)

View File

@ -274,7 +274,7 @@ def get_tab(win_id, target):
return tabbed_browser.tabopen(url=None, background=bg_tab) return tabbed_browser.tabopen(url=None, background=bg_tab)
def get_user_stylesheet(): def get_user_stylesheet(searching=False):
"""Get the combined user-stylesheet.""" """Get the combined user-stylesheet."""
css = '' css = ''
stylesheets = config.val.content.user_stylesheets stylesheets = config.val.content.user_stylesheets
@ -283,7 +283,8 @@ def get_user_stylesheet():
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
css += f.read() css += f.read()
if not config.val.scrolling.bar: if (config.val.scrolling.bar == 'never' or
config.val.scrolling.bar == 'when-searching' and not searching):
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
return css return css

View File

@ -162,8 +162,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
back yet. back yet.
""" """
def __init__(self, parent=None): def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(tab, parent)
self._flags = QWebEnginePage.FindFlags(0) self._flags = QWebEnginePage.FindFlags(0)
self._pending_searches = 0 self._pending_searches = 0
@ -191,8 +191,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
flag_text = '' flag_text = ''
log.webview.debug(' '.join([caller, found_text, text, flag_text]) log.webview.debug(' '.join([caller, found_text, text, flag_text])
.strip()) .strip())
if callback is not None: if callback is not None:
callback(found) callback(found)
self.finished.emit(found)
self._widget.findText(text, flags, wrapped_callback) self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case='never', reverse=False, def search(self, text, *, ignore_case='never', reverse=False,
@ -213,6 +216,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._find(text, self._flags, result_cb, 'search') self._find(text, self._flags, result_cb, 'search')
def clear(self): def clear(self):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False self.search_displayed = False
self._widget.findText('') self._widget.findText('')
@ -637,14 +642,26 @@ class WebEngineElements(browsertab.AbstractElements):
class WebEngineAudio(browsertab.AbstractAudio): class WebEngineAudio(browsertab.AbstractAudio):
"""QtWebEngine implemementations related to audio/muting.""" """QtWebEngine implemementations related to audio/muting.
Attributes:
_overridden: Whether the user toggled muting manually.
If that's the case, we leave it alone.
"""
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._overridden = False
def _connect_signals(self): def _connect_signals(self):
page = self._widget.page() page = self._widget.page()
page.audioMutedChanged.connect(self.muted_changed) page.audioMutedChanged.connect(self.muted_changed)
page.recentlyAudibleChanged.connect(self.recently_audible_changed) page.recentlyAudibleChanged.connect(self.recently_audible_changed)
self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_changed)
def set_muted(self, muted: bool): def set_muted(self, muted: bool, override: bool = False):
self._overridden = override
page = self._widget.page() page = self._widget.page()
page.setAudioMuted(muted) page.setAudioMuted(muted)
@ -656,6 +673,17 @@ class WebEngineAudio(browsertab.AbstractAudio):
page = self._widget.page() page = self._widget.page()
return page.recentlyAudible() return page.recentlyAudible()
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
if self._overridden:
return
mute = config.instance.get('content.mute', url=url)
self.set_muted(mute)
@config.change_filter('content.mute')
def _on_config_changed(self):
self._on_url_changed(self._tab.url())
class _WebEnginePermissions(QObject): class _WebEnginePermissions(QObject):
@ -812,17 +840,23 @@ class _WebEngineScripts(QObject):
self._greasemonkey = objreg.get('greasemonkey') self._greasemonkey = objreg.get('greasemonkey')
def connect_signals(self): def connect_signals(self):
"""Connect signals to our private slots."""
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
self._tab.search.cleared.connect(functools.partial(
self._update_stylesheet, searching=False))
self._tab.search.finished.connect(self._update_stylesheet)
@pyqtSlot(str) @pyqtSlot(str)
def _on_config_changed(self, option): def _on_config_changed(self, option):
if option in ['scrolling.bar', 'content.user_stylesheets']: if option in ['scrolling.bar', 'content.user_stylesheets']:
self._init_stylesheet() self._init_stylesheet()
self._update_stylesheet() self._update_stylesheet()
def _update_stylesheet(self): @pyqtSlot(bool)
def _update_stylesheet(self, searching=False):
"""Update the custom stylesheet in existing tabs.""" """Update the custom stylesheet in existing tabs."""
css = shared.get_user_stylesheet() css = shared.get_user_stylesheet(searching=searching)
code = javascript.assemble('stylesheet', 'set_css', css) code = javascript.assemble('stylesheet', 'set_css', css)
self._tab.run_js_async(code) self._tab.run_js_async(code)
@ -991,16 +1025,16 @@ class WebEngineTab(browsertab.AbstractTab):
private=private, parent=parent) private=private, parent=parent)
widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
private=private) private=private)
self.history = WebEngineHistory(self) self.history = WebEngineHistory(tab=self)
self.scroller = WebEngineScroller(self, parent=self) self.scroller = WebEngineScroller(tab=self, parent=self)
self.caret = WebEngineCaret(mode_manager=mode_manager, self.caret = WebEngineCaret(mode_manager=mode_manager,
tab=self, parent=self) tab=self, parent=self)
self.zoom = WebEngineZoom(tab=self, parent=self) self.zoom = WebEngineZoom(tab=self, parent=self)
self.search = WebEngineSearch(parent=self) self.search = WebEngineSearch(tab=self, parent=self)
self.printing = WebEnginePrinting(tab=self) self.printing = WebEnginePrinting(tab=self)
self.elements = WebEngineElements(tab=self) self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(tab=self) self.action = WebEngineAction(tab=self)
self.audio = WebEngineAudio(parent=self) self.audio = WebEngineAudio(tab=self, parent=self)
self._permissions = _WebEnginePermissions(tab=self, parent=self) self._permissions = _WebEnginePermissions(tab=self, parent=self)
self._scripts = _WebEngineScripts(tab=self, parent=self) self._scripts = _WebEngineScripts(tab=self, parent=self)
# We're assigning settings in _set_widget # We're assigning settings in _set_widget

View File

@ -84,8 +84,8 @@ class WebKitSearch(browsertab.AbstractSearch):
"""QtWebKit implementations related to searching on the page.""" """QtWebKit implementations related to searching on the page."""
def __init__(self, parent=None): def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(tab, parent)
self._flags = QWebPage.FindFlags(0) self._flags = QWebPage.FindFlags(0)
def _call_cb(self, callback, found, text, flags, caller): def _call_cb(self, callback, found, text, flags, caller):
@ -115,7 +115,11 @@ class WebKitSearch(browsertab.AbstractSearch):
if callback is not None: if callback is not None:
QTimer.singleShot(0, functools.partial(callback, found)) QTimer.singleShot(0, functools.partial(callback, found))
self.finished.emit(found)
def clear(self): def clear(self):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False self.search_displayed = False
# We first clear the marked text, then the highlights # We first clear the marked text, then the highlights
self._widget.findText('') self._widget.findText('')
@ -637,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit.""" """Dummy handling of audio status for QtWebKit."""
def set_muted(self, muted: bool): def set_muted(self, muted: bool, override: bool = False):
raise browsertab.WebTabError('Muting is not supported on QtWebKit!') raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self): def is_muted(self):
@ -658,16 +662,16 @@ class WebKitTab(browsertab.AbstractTab):
private=private, tab=self) private=private, tab=self)
if private: if private:
self._make_private(widget) self._make_private(widget)
self.history = WebKitHistory(self) self.history = WebKitHistory(tab=self)
self.scroller = WebKitScroller(self, parent=self) self.scroller = WebKitScroller(tab=self, parent=self)
self.caret = WebKitCaret(mode_manager=mode_manager, self.caret = WebKitCaret(mode_manager=mode_manager,
tab=self, parent=self) tab=self, parent=self)
self.zoom = WebKitZoom(tab=self, parent=self) self.zoom = WebKitZoom(tab=self, parent=self)
self.search = WebKitSearch(parent=self) self.search = WebKitSearch(tab=self, parent=self)
self.printing = WebKitPrinting(tab=self) self.printing = WebKitPrinting(tab=self)
self.elements = WebKitElements(tab=self) self.elements = WebKitElements(tab=self)
self.action = WebKitAction(tab=self) self.action = WebKitAction(tab=self)
self.audio = WebKitAudio(parent=self) self.audio = WebKitAudio(tab=self, parent=self)
# We're assigning settings in _set_widget # We're assigning settings in _set_widget
self.settings = webkitsettings.WebKitSettings(settings=None) self.settings = webkitsettings.WebKitSettings(settings=None)
self._set_widget(widget) self._set_widget(widget)

View File

@ -312,10 +312,14 @@ class Config(QObject):
name, deleted=deleted, renamed=renamed) name, deleted=deleted, renamed=renamed)
raise exception from None raise exception from None
def get(self, name, url=None): def get(self, name, url=None, *, fallback=True):
"""Get the given setting converted for Python code.""" """Get the given setting converted for Python code.
Args:
fallback: Use the global value if there's no URL-specific one.
"""
opt = self.get_opt(name) opt = self.get_opt(name)
obj = self.get_obj(name, url=url) obj = self.get_obj(name, url=url, fallback=fallback)
return opt.typ.to_py(obj) return opt.typ.to_py(obj)
def _maybe_copy(self, value): def _maybe_copy(self, value):
@ -329,14 +333,14 @@ class Config(QObject):
assert value.__hash__ is not None, value assert value.__hash__ is not None, value
return value return value
def get_obj(self, name, *, url=None): def get_obj(self, name, *, url=None, fallback=True):
"""Get the given setting as object (for YAML/config.py). """Get the given setting as object (for YAML/config.py).
Note that the returned values are not watched for mutation. Note that the returned values are not watched for mutation.
If a URL is given, return the value which should be used for that URL. If a URL is given, return the value which should be used for that URL.
""" """
self.get_opt(name) # To make sure it exists self.get_opt(name) # To make sure it exists
value = self._values[name].get_for_url(url) value = self._values[name].get_for_url(url, fallback=fallback)
return self._maybe_copy(value) return self._maybe_copy(value)
def get_obj_for_pattern(self, name, *, pattern): def get_obj_for_pattern(self, name, *, pattern):

View File

@ -806,6 +806,17 @@ content.xss_auditing:
Suspicious scripts will be blocked and reported in the inspector's Suspicious scripts will be blocked and reported in the inspector's
JavaScript console. JavaScript console.
content.mute:
default: false
type: Bool
supports_pattern: true
desc: >-
Automatically mute tabs.
Note that if the `:tab-mute` command is used, the mute status for the
affected tab is now controlled manually, and this setting doesn't have any
effect.
# emacs: ' # emacs: '
## completion ## completion
@ -1271,9 +1282,15 @@ prompt.radius:
## scrolling ## scrolling
scrolling.bar: scrolling.bar:
type: Bool type:
default: false name: String
desc: Show a scrollbar. valid_values:
- always: Always show the scrollbar.
- never: Never show the scrollbar.
- when-searching: Show the scrollbar when searching for text in the
webpage. With the QtWebKit backend, this is equal to `never`.
default: when-searching
desc: When to show the scrollbar.
scrolling.smooth: scrolling.smooth:
type: Bool type: Bool
@ -1602,6 +1619,23 @@ tabs.min_width:
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False. This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
tabs.max_width:
default: -1
type:
name: Int
minval: -1
maxval: maxint
desc: >-
Maximum width (in pixels) of tabs (-1 for no maximum).
This setting only applies when tabs are horizontal.
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is
False.
This setting may not apply properly if max_width is smaller than the
minimum size of tab contents, or smaller than tabs.min_width.
tabs.width.indicator: tabs.width.indicator:
renamed: tabs.indicator.width renamed: tabs.indicator.width

View File

@ -292,6 +292,8 @@ class YamlConfig(QObject):
self._mark_changed() self._mark_changed()
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never') self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
self._migrate_bool(settings, 'scrolling.bar',
'when-searching', 'never')
self._migrate_bool(settings, 'qt.force_software_rendering', self._migrate_bool(settings, 'qt.force_software_rendering',
'software-opengl', 'none') 'software-opengl', 'none')

View File

@ -60,7 +60,7 @@ from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc from qutebrowser.config import configexc, configutils
from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
@ -149,6 +149,9 @@ class BaseType:
value: The value to check. value: The value to check.
pytype: A Python type to check the value against. pytype: A Python type to check the value against.
""" """
if value is configutils.UNSET:
return
if (value is None or (pytype == list and value == []) or if (value is None or (pytype == list and value == []) or
(pytype == dict and value == {})): (pytype == dict and value == {})):
if not self.none_ok: if not self.none_ok:
@ -309,7 +312,9 @@ class MappingType(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
self._validate_valid_values(value.lower()) self._validate_valid_values(value.lower())
return self.MAPPING[value.lower()] return self.MAPPING[value.lower()]
@ -367,7 +372,9 @@ class String(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
self._validate_encoding(value) self._validate_encoding(value)
@ -399,7 +406,9 @@ class UniqueCharString(String):
def to_py(self, value): def to_py(self, value):
value = super().to_py(value) value = super().to_py(value)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
# Check for duplicate values # Check for duplicate values
@ -455,7 +464,9 @@ class List(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, list) self._basic_py_validation(value, list)
if not value: if value is configutils.UNSET:
return value
elif not value:
return [] return []
for val in value: for val in value:
@ -534,6 +545,9 @@ class ListOrValue(BaseType):
return value return value
def to_py(self, value): def to_py(self, value):
if value is configutils.UNSET:
return value
try: try:
return [self.valtype.to_py(value)] return [self.valtype.to_py(value)]
except configexc.ValidationError: except configexc.ValidationError:
@ -577,6 +591,7 @@ class FlagList(List):
def to_py(self, value): def to_py(self, value):
vals = super().to_py(value) vals = super().to_py(value)
if vals is not configutils.UNSET:
self._check_duplicates(vals) self._check_duplicates(vals)
return vals return vals
@ -764,7 +779,9 @@ class Perc(_Numeric):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, (float, int, str)) self._basic_py_validation(value, (float, int, str))
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if isinstance(value, str): if isinstance(value, str):
@ -907,7 +924,9 @@ class QtColor(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
color = QColor(value) color = QColor(value)
@ -936,7 +955,9 @@ class QssColor(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient',
@ -981,7 +1002,9 @@ class Font(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if not self.font_regex.fullmatch(value): # pragma: no cover if not self.font_regex.fullmatch(value): # pragma: no cover
@ -1000,7 +1023,9 @@ class FontFamily(Font):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
match = self.font_regex.fullmatch(value) match = self.font_regex.fullmatch(value)
@ -1024,7 +1049,9 @@ class QtFont(Font):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
style_map = { style_map = {
@ -1136,7 +1163,9 @@ class Regex(BaseType):
def to_py(self, value): def to_py(self, value):
"""Get a compiled regex from either a string or a regex object.""" """Get a compiled regex from either a string or a regex object."""
self._basic_py_validation(value, (str, self._regex_type)) self._basic_py_validation(value, (str, self._regex_type))
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
elif isinstance(value, str): elif isinstance(value, str):
return self._compile_regex(value) return self._compile_regex(value)
@ -1214,7 +1243,9 @@ class Dict(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, dict) self._basic_py_validation(value, dict)
if not value: if value is configutils.UNSET:
return value
elif not value:
return self._fill_fixed_keys({}) return self._fill_fixed_keys({})
self._validate_keys(value) self._validate_keys(value)
@ -1256,7 +1287,9 @@ class File(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
value = os.path.expanduser(value) value = os.path.expanduser(value)
@ -1282,7 +1315,9 @@ class Directory(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
value = os.path.expandvars(value) value = os.path.expandvars(value)
value = os.path.expanduser(value) value = os.path.expanduser(value)
@ -1309,7 +1344,9 @@ class FormatString(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1341,8 +1378,10 @@ class ShellCommand(List):
def to_py(self, value): def to_py(self, value):
value = super().to_py(value) value = super().to_py(value)
if not value: if value is configutils.UNSET:
return value return value
elif not value:
return []
if (self.placeholder and if (self.placeholder and
'{}' not in ' '.join(value) and '{}' not in ' '.join(value) and
@ -1365,7 +1404,9 @@ class Proxy(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1401,7 +1442,9 @@ class SearchEngineUrl(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if not ('{}' in value or '{0}' in value): if not ('{}' in value or '{0}' in value):
@ -1429,7 +1472,9 @@ class FuzzyUrl(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1463,6 +1508,9 @@ class Padding(Dict):
def to_py(self, value): def to_py(self, value):
d = super().to_py(value) d = super().to_py(value)
if d is configutils.UNSET:
return d
return PaddingValues(**d) return PaddingValues(**d)
@ -1472,7 +1520,9 @@ class Encoding(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
codecs.lookup(value) codecs.lookup(value)
@ -1529,7 +1579,9 @@ class Url(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
qurl = QUrl.fromUserInput(value) qurl = QUrl.fromUserInput(value)
@ -1545,7 +1597,9 @@ class SessionName(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if value.startswith('_'): if value.startswith('_'):
raise configexc.ValidationError(value, "may not start with '_'!") raise configexc.ValidationError(value, "may not start with '_'!")
@ -1593,8 +1647,10 @@ class ConfirmQuit(FlagList):
def to_py(self, value): def to_py(self, value):
values = super().to_py(value) values = super().to_py(value)
if not values: if values is configutils.UNSET:
return values return values
elif not values:
return []
# Never can't be set with other options # Never can't be set with other options
if 'never' in values and len(values) > 1: if 'never' in values and len(values) > 1:
@ -1630,7 +1686,9 @@ class TimestampTemplate(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1654,7 +1712,9 @@ class Key(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1673,7 +1733,9 @@ class UrlPattern(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:

View File

@ -642,6 +642,9 @@ class TabBar(QTabBar):
# Qt shrink us down. If for some reason (tests, bugs) # Qt shrink us down. If for some reason (tests, bugs)
# self.width() gives 0, use a sane min of 10 px # self.width() gives 0, use a sane min of 10 px
width = max(self.width(), 10) width = max(self.width(), 10)
max_width = config.cache['tabs.max_width']
if max_width > 0:
width = min(max_width, width)
size = QSize(width, height) size = QSize(width, height)
qtutils.ensure_valid(size) qtutils.ensure_valid(size)
return size return size

View File

@ -257,7 +257,7 @@ class FakeWebTab(browsertab.AbstractTab):
self.history = FakeWebTabHistory(self, can_go_back=can_go_back, self.history = FakeWebTabHistory(self, can_go_back=can_go_back,
can_go_forward=can_go_forward) can_go_forward=can_go_forward)
self.scroller = FakeWebTabScroller(self, scroll_pos_perc) self.scroller = FakeWebTabScroller(self, scroll_pos_perc)
self.audio = FakeWebTabAudio() self.audio = FakeWebTabAudio(self)
wrapped = QWidget() wrapped = QWidget()
self._layout.wrap(self, wrapped) self._layout.wrap(self, wrapped)

View File

@ -480,6 +480,17 @@ class TestConfig:
conf.set_obj(name, False, pattern=pattern) conf.set_obj(name, False, pattern=pattern)
assert conf.get(name, url=QUrl('https://example.com/')) is False assert conf.get(name, url=QUrl('https://example.com/')) is False
@pytest.mark.parametrize('fallback, expected', [
(True, True),
(False, configutils.UNSET)
])
def test_get_for_url_fallback(self, conf, fallback, expected):
"""Test conf.get() with an URL and fallback."""
value = conf.get('content.javascript.enabled',
url=QUrl('https://example.com/'),
fallback=fallback)
assert value is expected
@pytest.mark.parametrize('value', [{}, {'normal': {'a': 'nop'}}]) @pytest.mark.parametrize('value', [{}, {'normal': {'a': 'nop'}}])
def test_get_bindings(self, config_stub, conf, value): def test_get_bindings(self, config_stub, conf, value):
"""Test conf.get() with bindings which have missing keys.""" """Test conf.get() with bindings which have missing keys."""

View File

@ -34,7 +34,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
from PyQt5.QtNetwork import QNetworkProxy from PyQt5.QtNetwork import QNetworkProxy
from qutebrowser.config import configtypes, configexc from qutebrowser.config import configtypes, configexc, configutils
from qutebrowser.utils import debug, utils, qtutils, urlmatch from qutebrowser.utils import debug, utils, qtutils, urlmatch
from qutebrowser.browser.network import pac from qutebrowser.browser.network import pac
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
@ -274,6 +274,11 @@ class TestAll:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
meth(value) meth(value)
@pytest.mark.parametrize('none_ok', [True, False])
def test_unset(self, klass, none_ok):
typ = klass(none_ok=none_ok)
assert typ.to_py(configutils.UNSET) is configutils.UNSET
def test_to_str_none(self, klass): def test_to_str_none(self, klass):
assert klass().to_str(None) == '' assert klass().to_str(None) == ''

View File

@ -38,6 +38,7 @@ class TestTabWidget:
qtbot.addWidget(w) qtbot.addWidget(w)
monkeypatch.setattr(tabwidget.objects, 'backend', monkeypatch.setattr(tabwidget.objects, 'backend',
usertypes.Backend.QtWebKit) usertypes.Backend.QtWebKit)
w.show()
return w return w
def test_small_icon_doesnt_crash(self, widget, qtbot, fake_web_tab): def test_small_icon_doesnt_crash(self, widget, qtbot, fake_web_tab):
@ -120,6 +121,19 @@ class TestTabWidget:
benchmark(widget.update_tab_titles) benchmark(widget.update_tab_titles)
def test_tab_min_width(self, widget, fake_web_tab, config_stub, qtbot):
widget.addTab(fake_web_tab(), 'foobar')
widget.addTab(fake_web_tab(), 'foobar1')
min_size = widget.tabBar().tabRect(0).width() + 10
config_stub.val.tabs.min_width = min_size
assert widget.tabBar().tabRect(0).width() == min_size
def test_tab_max_width(self, widget, fake_web_tab, config_stub, qtbot):
widget.addTab(fake_web_tab(), 'foobar')
max_size = widget.tabBar().tabRect(0).width() - 10
config_stub.val.tabs.max_width = max_size
assert widget.tabBar().tabRect(0).width() == max_size
@pytest.mark.parametrize("num_tabs", [4, 100]) @pytest.mark.parametrize("num_tabs", [4, 100])
@pytest.mark.parametrize("rev", [True, False]) @pytest.mark.parametrize("rev", [True, False])
def test_add_remove_tab_benchmark(self, benchmark, widget, def test_add_remove_tab_benchmark(self, benchmark, widget,