This commit is contained in:
skepticspriggan 2026-01-07 16:35:38 -08:00 committed by GitHub
commit ce38941742
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 155 additions and 5 deletions

View File

@ -82,6 +82,7 @@ Added
* Loaded WebExtensions (partial support landed in QtWebEngine 6.10, no * Loaded WebExtensions (partial support landed in QtWebEngine 6.10, no
official qutebrowser support yet). official qutebrowser support yet).
- Support for hinting elements which are part of an (open) shadow DOM. - Support for hinting elements which are part of an (open) shadow DOM.
- Show zoom percentage in statusbar. (#2870)
Changed Changed
~~~~~~~ ~~~~~~~

View File

@ -434,6 +434,9 @@ class AbstractZoom(QObject):
"""Attribute ``zoom`` of AbstractTab for controlling zoom.""" """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: def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent) super().__init__(parent)
self._tab = tab self._tab = tab

View File

@ -728,8 +728,20 @@ class WebEngineZoom(browsertab.AbstractZoom):
_widget: webview.WebEngineView _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): def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor) self._widget.setZoomFactor(factor)
if not hasattr(self._widget.page(), "zoomFactorChanged"):
self.factor_changed.emit(factor)
class WebEngineElements(browsertab.AbstractElements): class WebEngineElements(browsertab.AbstractElements):
@ -1286,6 +1298,7 @@ class WebEngineTab(browsertab.AbstractTab):
search: WebEngineSearch search: WebEngineSearch
audio: WebEngineAudio audio: WebEngineAudio
printing: WebEnginePrinting printing: WebEnginePrinting
zoom: WebEngineZoom
def __init__(self, *, win_id, mode_manager, private, parent=None): def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, super().__init__(win_id=win_id,
@ -1760,5 +1773,6 @@ class WebEngineTab(browsertab.AbstractTab):
self.audio._connect_signals() self.audio._connect_signals()
self.search.connect_signals() self.search.connect_signals()
self.printing.connect_signals() self.printing.connect_signals()
self.zoom.connect_signals()
self._permissions.connect_signals() self._permissions.connect_signals()
self._scripts.connect_signals() self._scripts.connect_signals()

View File

@ -2208,10 +2208,21 @@ statusbar.widgets:
format string via `clock:...`. For supported format strings, see format string via `clock:...`. For supported format strings, see
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes[the https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes[the
Python datetime documentation]." Python datetime documentation]."
- zoom: "Display zoom percentage (e.g. `200%`)"
none_ok: true 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." 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
tabs.background: tabs.background:

View File

@ -525,6 +525,8 @@ class MainWindow(QWidget):
self.tabbed_browser.cur_scroll_perc_changed.connect( self.tabbed_browser.cur_scroll_perc_changed.connect(
self.status.percentage.set_perc) 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.tabbed_browser.widget.tab_index_changed.connect(
self.status.tabindex.on_tab_index_changed) self.status.tabindex.on_tab_index_changed)

View File

@ -16,7 +16,8 @@ from qutebrowser.keyinput import modeman
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (backforward, command, progress, from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
keystring, percentage, url, keystring, percentage, url,
tabindex, textbase, clock, searchmatch) tabindex, textbase, clock, searchmatch,
zoom)
@dataclasses.dataclass @dataclasses.dataclass
@ -190,6 +191,7 @@ class StatusBar(QWidget):
self.keystring = keystring.KeyString() self.keystring = keystring.KeyString()
self.prog = progress.Progress(self) self.prog = progress.Progress(self)
self.clock = clock.Clock() self.clock = clock.Clock()
self.zoom = zoom.Zoom()
self._text_widgets = [] self._text_widgets = []
self._draw_widgets() self._draw_widgets()
@ -223,6 +225,8 @@ class StatusBar(QWidget):
return new_text_widget return new_text_widget
elif key.startswith('clock:') or key == 'clock': elif key.startswith('clock:') or key == 'clock':
return self.clock return self.clock
elif key == 'zoom':
return self.zoom
else: else:
raise utils.Unreachable(key) raise utils.Unreachable(key)
@ -248,7 +252,7 @@ class StatusBar(QWidget):
if segment == 'scroll_raw': if segment == 'scroll_raw':
widget.set_raw() widget.set_raw()
elif segment in ('history', 'progress'): elif segment in ('history', 'progress', 'zoom'):
widget.enabled = True widget.enabled = True
if tab: if tab:
widget.on_tab_changed(tab) widget.on_tab_changed(tab)
@ -272,7 +276,8 @@ class StatusBar(QWidget):
# Start with widgets hidden and show them when needed # Start with widgets hidden and show them when needed
for widget in [self.url, self.percentage, for widget in [self.url, self.percentage,
self.backforward, self.tabindex, 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) assert isinstance(widget, QWidget)
if widget in [self.prog, self.backforward]: if widget in [self.prog, self.backforward]:
widget.enabled = False # type: ignore[attr-defined] widget.enabled = False # type: ignore[attr-defined]
@ -423,6 +428,7 @@ class StatusBar(QWidget):
self.prog.on_tab_changed(tab) self.prog.on_tab_changed(tab)
self.percentage.on_tab_changed(tab) self.percentage.on_tab_changed(tab)
self.backforward.on_tab_changed(tab) self.backforward.on_tab_changed(tab)
self.zoom.on_tab_changed(tab)
self.maybe_hide() self.maybe_hide()
assert tab.is_private == self._color_flags.private assert tab.is_private == self._color_flags.private

View File

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

View File

@ -168,6 +168,7 @@ class TabbedBrowser(QWidget):
cur_load_started: Current tab started loading (load_started) cur_load_started: Current tab started loading (load_started)
cur_load_finished: Current tab finished loading (load_finished) cur_load_finished: Current tab finished loading (load_finished)
cur_url_changed: Current URL changed. 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_link_hovered: Link hovered in current tab (link_hovered)
cur_scroll_perc_changed: Scroll percentage of current tab changed. cur_scroll_perc_changed: Scroll percentage of current tab changed.
arg 1: x-position in %. arg 1: x-position in %.
@ -189,6 +190,7 @@ class TabbedBrowser(QWidget):
cur_url_changed = pyqtSignal(QUrl) cur_url_changed = pyqtSignal(QUrl)
cur_link_hovered = pyqtSignal(str) cur_link_hovered = pyqtSignal(str)
cur_scroll_perc_changed = pyqtSignal(int, int) cur_scroll_perc_changed = pyqtSignal(int, int)
cur_zoom_changed = pyqtSignal(float)
cur_load_status_changed = pyqtSignal(usertypes.LoadStatus) cur_load_status_changed = pyqtSignal(usertypes.LoadStatus)
cur_search_match_changed = pyqtSignal(browsertab.SearchMatch) cur_search_match_changed = pyqtSignal(browsertab.SearchMatch)
cur_fullscreen_requested = pyqtSignal(bool) cur_fullscreen_requested = pyqtSignal(bool)
@ -337,6 +339,8 @@ class TabbedBrowser(QWidget):
self._filter.create(self.cur_load_started, tab)) self._filter.create(self.cur_load_started, tab))
tab.scroller.perc_changed.connect( tab.scroller.perc_changed.connect(
self._filter.create(self.cur_scroll_perc_changed, tab)) 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( tab.url_changed.connect(
self._filter.create(self.cur_url_changed, tab)) self._filter.create(self.cur_url_changed, tab))
tab.load_status_changed.connect( tab.load_status_changed.connect(

View File

@ -205,6 +205,18 @@ class FakeWebTabScroller(browsertab.AbstractScroller):
return self._pos_perc 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): class FakeWebTabHistory(browsertab.AbstractHistory):
"""Fake for Web{Kit,Engine}History.""" """Fake for Web{Kit,Engine}History."""
@ -245,7 +257,8 @@ class FakeWebTab(browsertab.AbstractTab):
def __init__(self, url=QUrl(), title='', tab_id=0, *, def __init__(self, url=QUrl(), title='', tab_id=0, *,
scroll_pos_perc=(0, 0), scroll_pos_perc=(0, 0),
load_status=usertypes.LoadStatus.success, 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) super().__init__(win_id=0, mode_manager=None, private=False)
self._load_status = load_status self._load_status = load_status
self._title = title self._title = title
@ -254,6 +267,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.zoom = FakeWebTabZoom(self, zoom_factor)
self.audio = FakeWebTabAudio(self) self.audio = FakeWebTabAudio(self)
self.private_api = FakeWebTabPrivate(tab=self, mode_manager=None) self.private_api = FakeWebTabPrivate(tab=self, mode_manager=None)
wrapped = QWidget() wrapped = QWidget()

View File

@ -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%]'