diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index c8bfe5258..28992a1ea 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -43,6 +43,10 @@ Added - 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. - 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 ~~~~~~~ @@ -73,6 +77,11 @@ Changed - Editing text in an external editor now simulates a JS "input" event, which improves compatibility with websites reacting via JS to input. - 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 ~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 99d8b1e33..9632f6331 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1413,7 +1413,7 @@ Yank something to the clipboard or primary selection. [[zoom]] === zoom -Syntax: +:zoom ['zoom']+ +Syntax: +:zoom [*--quiet*] ['zoom']+ 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 * +'zoom'+: The zoom percentage to set. +==== optional arguments +* +*-q*+, +*--quiet*+: Don't show a zoom level message. + ==== count The zoom percentage to set. [[zoom-in]] === zoom-in +Syntax: +:zoom-in [*--quiet*]+ + Increase the zoom level for the current tab. +==== optional arguments +* +*-q*+, +*--quiet*+: Don't show a zoom level message. + ==== count How many steps to zoom in. [[zoom-out]] === zoom-out +Syntax: +:zoom-out [*--quiet*]+ + Decrease the zoom level for the current tab. +==== optional arguments +* +*-q*+, +*--quiet*+: Don't show a zoom level message. + ==== count How many steps to zoom out. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index be3ceae77..962e9ebae 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -145,6 +145,7 @@ |<>|Enable support for HTML 5 local storage and Web SQL. |<>|Allow websites to record audio/video. |<>|Allow websites to lock your mouse pointer. +|<>|Automatically mute tabs. |<>|Netrc-file for HTTP authentication. |<>|Allow websites to show notifications. |<>|Allow pdf.js to view PDF files in the browser. @@ -231,7 +232,7 @@ |<>|Turn on Qt HighDPI scaling. |<>|When to use Chromium's low-end device mode. |<>|Which Chromium process model to use. -|<>|Show a scrollbar. +|<>|When to show the scrollbar. |<>|Enable smooth scrolling for web pages. |<>|When to find text on a page case-insensitively. |<>|Find text on a page incrementally, renewing the search for each typed character. @@ -250,6 +251,7 @@ |<>|Padding (in pixels) for tab indicators. |<>|Width (in pixels) of the progress indicator (0 to disable). |<>|How to behave when the last tab is closed. +|<>|Maximum width (in pixels) of tabs (-1 for no maximum). |<>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior). |<>|When switching tabs, what input mode is applied. |<>|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. +[[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: <> + +Default: +pass:[false]+ + [[content.netrc_file]] === content.netrc_file Netrc-file for HTTP authentication. @@ -2815,11 +2828,17 @@ This setting is only available with the QtWebEngine backend. [[scrolling.bar]] === scrolling.bar -Show a scrollbar. +When to show the scrollbar. -Type: <> +Type: <> -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 @@ -3086,6 +3105,17 @@ Valid values: 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: <> + +Default: +pass:[-1]+ + [[tabs.min_width]] === tabs.min_width Minimum width (in pixels) of tabs (-1 for the default minimum size behavior). diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 0eb9b6493..eb67cf091 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -248,10 +248,19 @@ class AbstractSearch(QObject): this view. _flags: The flags of the last search (needs to be set by subclasses). _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) + self._tab = tab self._widget = None self.text = None self.search_displayed = False @@ -668,20 +677,27 @@ class AbstractAudio(QObject): muted_changed = pyqtSignal(bool) recently_audible_changed = pyqtSignal(bool) - def __init__(self, parent=None): + def __init__(self, tab, parent=None): super().__init__(parent) self._widget = None + self._tab = tab - def set_muted(self, muted: bool): - """Set this tab as muted or not.""" + def set_muted(self, muted: bool, override: bool = False): + """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 def is_muted(self): """Whether this tab is muted.""" raise NotImplementedError - def toggle_muted(self): - self.set_muted(not self.is_muted()) + def toggle_muted(self, *, override: bool = False): + self.set_muted(not self.is_muted(), override=override) def is_recently_audible(self): """Whether this tab has had audio playing recently.""" diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 6cdbc0246..d4e896cf7 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -870,37 +870,41 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @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. Args: count: How many steps to zoom in. + quiet: Don't show a zoom level message. """ tab = self._current_widget() try: perc = tab.zoom.offset(count) except ValueError as e: raise cmdexc.CommandError(e) - message.info("Zoom level: {}%".format(int(perc)), replace=True) + if not quiet: + message.info("Zoom level: {}%".format(int(perc)), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') @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. Args: count: How many steps to zoom out. + quiet: Don't show a zoom level message. """ tab = self._current_widget() try: perc = tab.zoom.offset(-count) except ValueError as e: raise cmdexc.CommandError(e) - message.info("Zoom level: {}%".format(int(perc)), replace=True) + if not quiet: + message.info("Zoom level: {}%".format(int(perc)), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') @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. The zoom can be given as argument or as [count]. If neither is @@ -910,6 +914,7 @@ class CommandDispatcher: Args: zoom: The zoom percentage to set. count: The zoom percentage to set. + quiet: Don't show a zoom level message. """ if zoom is not None: try: @@ -927,7 +932,8 @@ class CommandDispatcher: tab.zoom.set_factor(float(level) / 100) except ValueError: raise cmdexc.CommandError("Can't zoom {}%!".format(level)) - message.info("Zoom level: {}%".format(int(level)), replace=True) + if not quiet: + message.info("Zoom level: {}%".format(int(level)), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') def tab_only(self, prev=False, next_=False, force=False): @@ -2231,6 +2237,6 @@ class CommandDispatcher: if tab is None: return try: - tab.audio.toggle_muted() + tab.audio.toggle_muted(override=True) except browsertab.WebTabError as e: raise cmdexc.CommandError(e) diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 2398ca2e4..eb222cbe8 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -274,7 +274,7 @@ def get_tab(win_id, target): return tabbed_browser.tabopen(url=None, background=bg_tab) -def get_user_stylesheet(): +def get_user_stylesheet(searching=False): """Get the combined user-stylesheet.""" css = '' stylesheets = config.val.content.user_stylesheets @@ -283,7 +283,8 @@ def get_user_stylesheet(): with open(filename, 'r', encoding='utf-8') as f: 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; }' return css diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 36ac2a99a..3b72719e7 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -162,8 +162,8 @@ class WebEngineSearch(browsertab.AbstractSearch): back yet. """ - def __init__(self, parent=None): - super().__init__(parent) + def __init__(self, tab, parent=None): + super().__init__(tab, parent) self._flags = QWebEnginePage.FindFlags(0) self._pending_searches = 0 @@ -191,8 +191,11 @@ class WebEngineSearch(browsertab.AbstractSearch): flag_text = '' log.webview.debug(' '.join([caller, found_text, text, flag_text]) .strip()) + if callback is not None: callback(found) + self.finished.emit(found) + self._widget.findText(text, flags, wrapped_callback) 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') def clear(self): + if self.search_displayed: + self.cleared.emit() self.search_displayed = False self._widget.findText('') @@ -637,14 +642,26 @@ class WebEngineElements(browsertab.AbstractElements): 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): page = self._widget.page() page.audioMutedChanged.connect(self.muted_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.setAudioMuted(muted) @@ -656,6 +673,17 @@ class WebEngineAudio(browsertab.AbstractAudio): page = self._widget.page() 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): @@ -812,17 +840,23 @@ class _WebEngineScripts(QObject): self._greasemonkey = objreg.get('greasemonkey') def connect_signals(self): + """Connect signals to our private slots.""" 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) def _on_config_changed(self, option): if option in ['scrolling.bar', 'content.user_stylesheets']: self._init_stylesheet() self._update_stylesheet() - def _update_stylesheet(self): + @pyqtSlot(bool) + def _update_stylesheet(self, searching=False): """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) self._tab.run_js_async(code) @@ -991,16 +1025,16 @@ class WebEngineTab(browsertab.AbstractTab): private=private, parent=parent) widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, private=private) - self.history = WebEngineHistory(self) - self.scroller = WebEngineScroller(self, parent=self) + self.history = WebEngineHistory(tab=self) + self.scroller = WebEngineScroller(tab=self, parent=self) self.caret = WebEngineCaret(mode_manager=mode_manager, 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.elements = WebEngineElements(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._scripts = _WebEngineScripts(tab=self, parent=self) # We're assigning settings in _set_widget diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 7f0740b65..2edea1777 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -84,8 +84,8 @@ class WebKitSearch(browsertab.AbstractSearch): """QtWebKit implementations related to searching on the page.""" - def __init__(self, parent=None): - super().__init__(parent) + def __init__(self, tab, parent=None): + super().__init__(tab, parent) self._flags = QWebPage.FindFlags(0) def _call_cb(self, callback, found, text, flags, caller): @@ -115,7 +115,11 @@ class WebKitSearch(browsertab.AbstractSearch): if callback is not None: QTimer.singleShot(0, functools.partial(callback, found)) + self.finished.emit(found) + def clear(self): + if self.search_displayed: + self.cleared.emit() self.search_displayed = False # We first clear the marked text, then the highlights self._widget.findText('') @@ -637,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio): """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!') def is_muted(self): @@ -658,16 +662,16 @@ class WebKitTab(browsertab.AbstractTab): private=private, tab=self) if private: self._make_private(widget) - self.history = WebKitHistory(self) - self.scroller = WebKitScroller(self, parent=self) + self.history = WebKitHistory(tab=self) + self.scroller = WebKitScroller(tab=self, parent=self) self.caret = WebKitCaret(mode_manager=mode_manager, 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.elements = WebKitElements(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 self.settings = webkitsettings.WebKitSettings(settings=None) self._set_widget(widget) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 98f2c67b3..facfcc553 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -312,10 +312,14 @@ class Config(QObject): name, deleted=deleted, renamed=renamed) raise exception from None - def get(self, name, url=None): - """Get the given setting converted for Python code.""" + def get(self, name, url=None, *, fallback=True): + """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) - obj = self.get_obj(name, url=url) + obj = self.get_obj(name, url=url, fallback=fallback) return opt.typ.to_py(obj) def _maybe_copy(self, value): @@ -329,14 +333,14 @@ class Config(QObject): assert value.__hash__ is not None, 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). 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. """ 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) def get_obj_for_pattern(self, name, *, pattern): diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index c4a61e203..ae9351899 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -806,6 +806,17 @@ content.xss_auditing: Suspicious scripts will be blocked and reported in the inspector's 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: ' ## completion @@ -1271,9 +1282,15 @@ prompt.radius: ## scrolling scrolling.bar: - type: Bool - default: false - desc: Show a scrollbar. + type: + name: String + 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: type: Bool @@ -1602,6 +1619,23 @@ tabs.min_width: 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: renamed: tabs.indicator.width diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index f74c44f32..b4c8ea4ec 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -292,6 +292,8 @@ class YamlConfig(QObject): self._mark_changed() 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', 'software-opengl', 'none') diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 1c3bff8fb..f4857fc36 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -60,7 +60,7 @@ from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QTabWidget, QTabBar 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.keyinput import keyutils @@ -149,6 +149,9 @@ class BaseType: value: The value to check. 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 (pytype == dict and value == {})): if not self.none_ok: @@ -309,7 +312,9 @@ class MappingType(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None self._validate_valid_values(value.lower()) return self.MAPPING[value.lower()] @@ -367,7 +372,9 @@ class String(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None self._validate_encoding(value) @@ -399,7 +406,9 @@ class UniqueCharString(String): def to_py(self, value): value = super().to_py(value) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None # Check for duplicate values @@ -455,7 +464,9 @@ class List(BaseType): def to_py(self, value): self._basic_py_validation(value, list) - if not value: + if value is configutils.UNSET: + return value + elif not value: return [] for val in value: @@ -534,6 +545,9 @@ class ListOrValue(BaseType): return value def to_py(self, value): + if value is configutils.UNSET: + return value + try: return [self.valtype.to_py(value)] except configexc.ValidationError: @@ -577,7 +591,8 @@ class FlagList(List): def to_py(self, value): vals = super().to_py(value) - self._check_duplicates(vals) + if vals is not configutils.UNSET: + self._check_duplicates(vals) return vals def complete(self): @@ -764,7 +779,9 @@ class Perc(_Numeric): def to_py(self, value): self._basic_py_validation(value, (float, int, str)) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None if isinstance(value, str): @@ -907,7 +924,9 @@ class QtColor(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None color = QColor(value) @@ -936,7 +955,9 @@ class QssColor(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', @@ -981,7 +1002,9 @@ class Font(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None if not self.font_regex.fullmatch(value): # pragma: no cover @@ -1000,7 +1023,9 @@ class FontFamily(Font): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None match = self.font_regex.fullmatch(value) @@ -1024,7 +1049,9 @@ class QtFont(Font): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None style_map = { @@ -1136,7 +1163,9 @@ class Regex(BaseType): def to_py(self, value): """Get a compiled regex from either a string or a regex object.""" self._basic_py_validation(value, (str, self._regex_type)) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None elif isinstance(value, str): return self._compile_regex(value) @@ -1214,7 +1243,9 @@ class Dict(BaseType): def to_py(self, value): self._basic_py_validation(value, dict) - if not value: + if value is configutils.UNSET: + return value + elif not value: return self._fill_fixed_keys({}) self._validate_keys(value) @@ -1256,7 +1287,9 @@ class File(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None value = os.path.expanduser(value) @@ -1282,7 +1315,9 @@ class Directory(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None value = os.path.expandvars(value) value = os.path.expanduser(value) @@ -1309,7 +1344,9 @@ class FormatString(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: @@ -1341,8 +1378,10 @@ class ShellCommand(List): def to_py(self, value): value = super().to_py(value) - if not value: + if value is configutils.UNSET: return value + elif not value: + return [] if (self.placeholder and '{}' not in ' '.join(value) and @@ -1365,7 +1404,9 @@ class Proxy(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: @@ -1401,7 +1442,9 @@ class SearchEngineUrl(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None if not ('{}' in value or '{0}' in value): @@ -1429,7 +1472,9 @@ class FuzzyUrl(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: @@ -1463,6 +1508,9 @@ class Padding(Dict): def to_py(self, value): d = super().to_py(value) + if d is configutils.UNSET: + return d + return PaddingValues(**d) @@ -1472,7 +1520,9 @@ class Encoding(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: codecs.lookup(value) @@ -1529,7 +1579,9 @@ class Url(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None qurl = QUrl.fromUserInput(value) @@ -1545,7 +1597,9 @@ class SessionName(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None if value.startswith('_'): raise configexc.ValidationError(value, "may not start with '_'!") @@ -1593,8 +1647,10 @@ class ConfirmQuit(FlagList): def to_py(self, value): values = super().to_py(value) - if not values: + if values is configutils.UNSET: return values + elif not values: + return [] # Never can't be set with other options if 'never' in values and len(values) > 1: @@ -1630,7 +1686,9 @@ class TimestampTemplate(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: @@ -1654,7 +1712,9 @@ class Key(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: @@ -1673,7 +1733,9 @@ class UrlPattern(BaseType): def to_py(self, value): self._basic_py_validation(value, str) - if not value: + if value is configutils.UNSET: + return value + elif not value: return None try: diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 69f0b1b92..5a70304db 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -642,6 +642,9 @@ class TabBar(QTabBar): # Qt shrink us down. If for some reason (tests, bugs) # self.width() gives 0, use a sane min of 10 px 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) qtutils.ensure_valid(size) return size diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 9af7a6fcc..173135d73 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -257,7 +257,7 @@ class FakeWebTab(browsertab.AbstractTab): self.history = FakeWebTabHistory(self, can_go_back=can_go_back, can_go_forward=can_go_forward) self.scroller = FakeWebTabScroller(self, scroll_pos_perc) - self.audio = FakeWebTabAudio() + self.audio = FakeWebTabAudio(self) wrapped = QWidget() self._layout.wrap(self, wrapped) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index bf4f7c02d..6cef14130 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -480,6 +480,17 @@ class TestConfig: conf.set_obj(name, False, pattern=pattern) 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'}}]) def test_get_bindings(self, config_stub, conf, value): """Test conf.get() with bindings which have missing keys.""" diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 46119be7e..ee5223a38 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -34,7 +34,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QFont 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.browser.network import pac from qutebrowser.keyinput import keyutils @@ -274,6 +274,11 @@ class TestAll: with pytest.raises(configexc.ValidationError): 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): assert klass().to_str(None) == '' diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index bc5ee9deb..24acb6d87 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -38,6 +38,7 @@ class TestTabWidget: qtbot.addWidget(w) monkeypatch.setattr(tabwidget.objects, 'backend', usertypes.Backend.QtWebKit) + w.show() return w def test_small_icon_doesnt_crash(self, widget, qtbot, fake_web_tab): @@ -120,6 +121,19 @@ class TestTabWidget: 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("rev", [True, False]) def test_add_remove_tab_benchmark(self, benchmark, widget,