diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index bdc7529a3..978d746de 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -82,6 +82,7 @@ Added * Loaded WebExtensions (partial support landed in QtWebEngine 6.10, no official qutebrowser support yet). - Support for hinting elements which are part of an (open) shadow DOM. +- Show zoom percentage in statusbar. (#2870) Changed ~~~~~~~ diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 597b8d37c..0c72dd25f 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -434,6 +434,9 @@ class AbstractZoom(QObject): """Attribute ``zoom`` of AbstractTab for controlling zoom.""" + #: Signal emitted when a tab's zoom factor changed (float) + factor_changed = pyqtSignal(float) + def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) self._tab = tab diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 726a2efab..7ebd4238a 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -728,8 +728,20 @@ class WebEngineZoom(browsertab.AbstractZoom): _widget: webview.WebEngineView + def connect_signals(self): + """Called from WebEngineTab.connect_signals.""" + page = self._widget.page() + if machinery.IS_QT6: + try: + page.zoomFactorChanged.connect(self.factor_changed) + except AttributeError: + # Added in Qt 6.8 + pass + def _set_factor_internal(self, factor): self._widget.setZoomFactor(factor) + if not hasattr(self._widget.page(), "zoomFactorChanged"): + self.factor_changed.emit(factor) class WebEngineElements(browsertab.AbstractElements): @@ -1286,6 +1298,7 @@ class WebEngineTab(browsertab.AbstractTab): search: WebEngineSearch audio: WebEngineAudio printing: WebEnginePrinting + zoom: WebEngineZoom def __init__(self, *, win_id, mode_manager, private, parent=None): super().__init__(win_id=win_id, @@ -1760,5 +1773,6 @@ class WebEngineTab(browsertab.AbstractTab): self.audio._connect_signals() self.search.connect_signals() self.printing.connect_signals() + self.zoom.connect_signals() self._permissions.connect_signals() self._scripts.connect_signals() diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index b221a70dc..adeca8b45 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2208,10 +2208,21 @@ statusbar.widgets: format string via `clock:...`. For supported format strings, see https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes[the Python datetime documentation]." + - zoom: "Display zoom percentage (e.g. `200%`)" none_ok: true - default: ['keypress', 'search_match', 'url', 'scroll', 'history', 'tabs', 'progress'] + default: ['keypress', 'search_match', 'url', 'zoom', 'scroll', + 'history', 'tabs', 'progress'] desc: "List of widgets displayed in the statusbar." +statusbar.zoom.show: + default: non-default + type: + name: String + valid_values: + - always: Always show the zoom percentage. + - non-default: Show the zoom percentage when it is not 100%. + desc: When to show the zoom percentage in the statusbar. + ## tabs tabs.background: diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index dcbaf589d..9924e5110 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -525,6 +525,8 @@ class MainWindow(QWidget): self.tabbed_browser.cur_scroll_perc_changed.connect( self.status.percentage.set_perc) + self.tabbed_browser.cur_zoom_changed.connect( + self.status.zoom.on_zoom_changed) self.tabbed_browser.widget.tab_index_changed.connect( self.status.tabindex.on_tab_index_changed) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index cce9e2c39..e7c6016f8 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -16,7 +16,8 @@ from qutebrowser.keyinput import modeman from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.mainwindow.statusbar import (backforward, command, progress, keystring, percentage, url, - tabindex, textbase, clock, searchmatch) + tabindex, textbase, clock, searchmatch, + zoom) @dataclasses.dataclass @@ -190,6 +191,7 @@ class StatusBar(QWidget): self.keystring = keystring.KeyString() self.prog = progress.Progress(self) self.clock = clock.Clock() + self.zoom = zoom.Zoom() self._text_widgets = [] self._draw_widgets() @@ -223,6 +225,8 @@ class StatusBar(QWidget): return new_text_widget elif key.startswith('clock:') or key == 'clock': return self.clock + elif key == 'zoom': + return self.zoom else: raise utils.Unreachable(key) @@ -248,7 +252,7 @@ class StatusBar(QWidget): if segment == 'scroll_raw': widget.set_raw() - elif segment in ('history', 'progress'): + elif segment in ('history', 'progress', 'zoom'): widget.enabled = True if tab: widget.on_tab_changed(tab) @@ -272,7 +276,8 @@ class StatusBar(QWidget): # Start with widgets hidden and show them when needed for widget in [self.url, self.percentage, self.backforward, self.tabindex, - self.keystring, self.prog, self.clock, *self._text_widgets]: + self.keystring, self.prog, self.clock, + self.zoom, *self._text_widgets]: assert isinstance(widget, QWidget) if widget in [self.prog, self.backforward]: widget.enabled = False # type: ignore[attr-defined] @@ -423,6 +428,7 @@ class StatusBar(QWidget): self.prog.on_tab_changed(tab) self.percentage.on_tab_changed(tab) self.backforward.on_tab_changed(tab) + self.zoom.on_tab_changed(tab) self.maybe_hide() assert tab.is_private == self._color_flags.private diff --git a/qutebrowser/mainwindow/statusbar/zoom.py b/qutebrowser/mainwindow/statusbar/zoom.py new file mode 100644 index 000000000..f24c4d60d --- /dev/null +++ b/qutebrowser/mainwindow/statusbar/zoom.py @@ -0,0 +1,29 @@ +"""Zoom percentage displayed in the statusbar.""" + +from qutebrowser.browser import browsertab +from qutebrowser.mainwindow.statusbar import textbase +from qutebrowser.qt.core import pyqtSlot, QObject +from qutebrowser.config import config + + +class Zoom(textbase.TextBase): + + """Shows zoom percentage in current tab.""" + + def __init__(self, parent: QObject = None) -> None: + super().__init__(parent) + self.on_zoom_changed(1) + + @pyqtSlot(float) + def on_zoom_changed(self, factor: float) -> None: + """Update percentage when factor changed.""" + if factor == 1 and config.val.statusbar.zoom.show == 'non-default': + self.hide() + return + self.show() + percentage = round(100 * factor) + self.setText(f"[{percentage}%]") + + def on_tab_changed(self, tab: browsertab.AbstractTab) -> None: + """Update percentage when tab changed.""" + self.on_zoom_changed(tab.zoom.factor()) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index e0938ae36..9b33387cf 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -168,6 +168,7 @@ class TabbedBrowser(QWidget): cur_load_started: Current tab started loading (load_started) cur_load_finished: Current tab finished loading (load_finished) cur_url_changed: Current URL changed. + cur_zoom_changed: Zoom factor of current tab changed. cur_link_hovered: Link hovered in current tab (link_hovered) cur_scroll_perc_changed: Scroll percentage of current tab changed. arg 1: x-position in %. @@ -189,6 +190,7 @@ class TabbedBrowser(QWidget): cur_url_changed = pyqtSignal(QUrl) cur_link_hovered = pyqtSignal(str) cur_scroll_perc_changed = pyqtSignal(int, int) + cur_zoom_changed = pyqtSignal(float) cur_load_status_changed = pyqtSignal(usertypes.LoadStatus) cur_search_match_changed = pyqtSignal(browsertab.SearchMatch) cur_fullscreen_requested = pyqtSignal(bool) @@ -337,6 +339,8 @@ class TabbedBrowser(QWidget): self._filter.create(self.cur_load_started, tab)) tab.scroller.perc_changed.connect( self._filter.create(self.cur_scroll_perc_changed, tab)) + tab.zoom.factor_changed.connect( + self._filter.create(self.cur_zoom_changed, tab)) tab.url_changed.connect( self._filter.create(self.cur_url_changed, tab)) tab.load_status_changed.connect( diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 50528ee8b..7a5011724 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -205,6 +205,18 @@ class FakeWebTabScroller(browsertab.AbstractScroller): return self._pos_perc +class FakeWebTabZoom(browsertab.AbstractZoom): + + """Fake AbstractZoom to use in tests.""" + + def __init__(self, tab, factor): + super().__init__(tab) + self._factor = factor + + def factor(self): + return self._factor + + class FakeWebTabHistory(browsertab.AbstractHistory): """Fake for Web{Kit,Engine}History.""" @@ -245,7 +257,8 @@ class FakeWebTab(browsertab.AbstractTab): def __init__(self, url=QUrl(), title='', tab_id=0, *, scroll_pos_perc=(0, 0), load_status=usertypes.LoadStatus.success, - progress=0, can_go_back=None, can_go_forward=None): + progress=0, can_go_back=None, can_go_forward=None, + zoom_factor=1): super().__init__(win_id=0, mode_manager=None, private=False) self._load_status = load_status self._title = title @@ -254,6 +267,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.zoom = FakeWebTabZoom(self, zoom_factor) self.audio = FakeWebTabAudio(self) self.private_api = FakeWebTabPrivate(tab=self, mode_manager=None) wrapped = QWidget() diff --git a/tests/unit/mainwindow/statusbar/test_zoom.py b/tests/unit/mainwindow/statusbar/test_zoom.py new file mode 100644 index 000000000..d7f3eb80f --- /dev/null +++ b/tests/unit/mainwindow/statusbar/test_zoom.py @@ -0,0 +1,66 @@ +"""Test Zoom widget.""" + +import pytest + +from qutebrowser.mainwindow.statusbar.zoom import Zoom +from typing import Any +import pytestqt.qtbot + + +@pytest.fixture +def zoom(qtbot: pytestqt.qtbot.QtBot, config_stub: Any) -> Zoom: + """Fixture providing a Zoom widget.""" + widget = Zoom() + qtbot.add_widget(widget) + return widget + + +@pytest.mark.parametrize('factor, expected', [ + (0.25, '[25%]'), + (0.5, '[50%]'), + (0.75, '[75%]'), + (1.5, '[150%]'), + (2, '[200%]'), + (3, '[300%]'), + (4, '[400%]'), + (5, '[500%]'), +]) +@pytest.mark.parametrize("show", ["non-default", "always"]) +def test_percentage_texts(zoom: Zoom, factor: float, show: str, expected: str, + config_stub: Any) -> None: + """Test text displayed by the widget based on the zoom factor of a tab and a config value. + + Args: + factor: zoom factor of the tab as a float. + show: config value for `statusbar.zoom.show`. + expected: expected text given factor. + """ + config_stub.val.statusbar.zoom.show = show + zoom.on_zoom_changed(factor=factor) + assert zoom.text() == expected + + +@pytest.mark.parametrize('show, expected', [ + ("always", '[100%]'), + ("non-default", ''), +]) +def test_default_percentage_text(zoom: Zoom, show: str, expected: str, + config_stub: Any) -> None: + """Test default percentage text based on a config value. + + Args: + show: config value for `statusbar.zoom.show`. + expected: expected text given show config value. + """ + config_stub.val.statusbar.zoom.show = show + zoom.on_zoom_changed(factor=1) + assert zoom.text() == expected + + +def test_tab_change(zoom: Zoom, fake_web_tab: Any) -> None: + """Test zoom factor change when switching tabs.""" + zoom.on_zoom_changed(factor=2) + assert zoom.text() == '[200%]' + tab = fake_web_tab(zoom_factor=0.5) + zoom.on_tab_changed(tab) + assert zoom.text() == '[50%]'