diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index e8dbf91c4..7f59c6c2e 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -111,6 +111,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Select the tab given as argument/[count]. |<>|Give the current tab to a new or existing window if win_id given. |<>|Move the current tab according to the argument and [count]. +|<>|Mute/Unmute the current/[count]th tab. |<>|Switch to the next tab, or switch [count] tabs forward. |<>|Close all tabs except for the current one. |<>|Pin/Unpin the current/[count]th tab. @@ -1284,6 +1285,13 @@ If moving relatively: Offset. If moving absolutely: New position (default: 0). T overrides the index argument, if given. +[[tab-mute]] +=== tab-mute +Mute/Unmute the current/[count]th tab. + +==== count +The tab index to pin or unpin + [[tab-next]] === tab-next Switch to the next tab, or switch [count] tabs forward. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index bd6d17995..fcf144787 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -3062,6 +3062,7 @@ The following placeholders are defined: * `{private}`: Indicates when private mode is enabled. * `{current_url}`: URL of the current web page. * `{protocol}`: Protocol (http/https/...) of the current web page. +* `{muted}`: Icon if the tab is muted Type: <> diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 809c643c3..c285ba764 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -656,6 +656,8 @@ class AbstractTab(QWidget): fullscreen_requested = pyqtSignal(bool) renderer_process_terminated = pyqtSignal(TerminationStatus, int) predicted_navigation = pyqtSignal(QUrl) + audio_muted_changed = pyqtSignal(bool) + recently_audible_changed = pyqtSignal(bool) def __init__(self, *, win_id, mode_manager, private, parent=None): self.private = private @@ -930,3 +932,15 @@ class AbstractTab(QWidget): def is_deleted(self): return sip.isdeleted(self._widget) + + def set_muted(self, muted: bool): + """Set this tab as muted or not.""" + raise NotImplementedError + + def is_muted(self): + """Whether this tab is muted.""" + raise NotImplementedError + + def is_recently_audible(self): + """Whether this tab has had audio playing recently.""" + raise NotImplementedError diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 45d1d2015..796c26b39 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -2224,3 +2224,20 @@ class CommandDispatcher: window = self._tabbed_browser.widget.window() window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) + + @cmdutils.register(instance='command-dispatcher', scope='window', + name='tab-mute') + @cmdutils.argument('count', count=True) + def tab_mute(self, count=None): + """Mute/Unmute the current/[count]th tab. + + Args: + count: The tab index to mute or unmute, or None + """ + tab = self._cntwidget(count) + if tab is None: + return + try: + tab.set_muted(not tab.is_muted()) + except browsertab.WebTabError as e: + raise cmdexc.CommandError(e) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 16b6f6e00..cc6f2b5ce 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1118,6 +1118,8 @@ class WebEngineTab(browsertab.AbstractTab): page.fullScreenRequested.connect(self._on_fullscreen_requested) page.contentsSizeChanged.connect(self.contents_size_changed) page.navigation_request.connect(self._on_navigation_request) + page.audioMutedChanged.connect(self.audio_muted_changed) + page.recentlyAudibleChanged.connect(self.recently_audible_changed) view.titleChanged.connect(self.title_changed) view.urlChanged.connect(self._on_url_changed) @@ -1142,3 +1144,15 @@ class WebEngineTab(browsertab.AbstractTab): def event_target(self): return self._widget.render_widget() + + def set_muted(self, muted: bool): + page = self._widget.page() + page.setAudioMuted(muted) + + def is_muted(self): + page = self._widget.page() + return page.isAudioMuted() + + def is_recently_audible(self): + page = self._widget.page() + return page.recentlyAudible() diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index a9f4f9840..1681bffdb 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -839,3 +839,14 @@ class WebKitTab(browsertab.AbstractTab): def event_target(self): return self._widget + + def set_muted(self, muted: bool): + raise browsertab.WebTabError('Muting is not supported on QtWebKit!') + + def is_muted(self): + # Dummy value for things that read muted status + return False + + def is_recently_audible(self): + # Dummy value for things that read audible status + return False diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index c6a0443f7..b9b41076d 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1359,7 +1359,7 @@ tabs.title.alignment: desc: Alignment of the text inside of tabs. tabs.title.format: - default: '{index}: {title}' + default: '{audio}{index}: {title}' type: name: FormatString fields: @@ -1374,6 +1374,7 @@ tabs.title.format: - private - current_url - protocol + - audio none_ok: true desc: | Format to use for the tab title. @@ -1391,6 +1392,7 @@ tabs.title.format: * `{private}`: Indicates when private mode is enabled. * `{current_url}`: URL of the current web page. * `{protocol}`: Protocol (http/https/...) of the current web page. + * `{audio}`: Indicator for audio/mute status tabs.title.format_pinned: default: '{index}' @@ -1408,6 +1410,7 @@ tabs.title.format_pinned: - private - current_url - protocol + - audio none_ok: true desc: Format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined. @@ -1575,6 +1578,7 @@ window.title_format: - private - current_url - protocol + - audio default: '{perc}{title}{title_sep}qutebrowser' desc: | Format to use for the window title. The same placeholders like for @@ -2378,6 +2382,7 @@ bindings.default: : follow-selected -t .: repeat-command : tab-pin + : tab-mute q: record-macro "@": run-macro tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index a35e8d7ef..27d2581f0 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -238,6 +238,10 @@ class TabbedBrowser(QWidget): functools.partial(self.on_window_close_requested, tab)) tab.renderer_process_terminated.connect( functools.partial(self._on_renderer_process_terminated, tab)) + tab.audio_muted_changed.connect( + functools.partial(self._on_audio_changed, tab)) + tab.recently_audible_changed.connect( + functools.partial(self._on_audio_changed, tab)) tab.new_tab_requested.connect(self.tabopen) if not self.private: web_history = objreg.get('web-history') @@ -734,6 +738,17 @@ class TabbedBrowser(QWidget): self._update_window_title('scroll_pos') self.widget.update_tab_title(idx, 'scroll_pos') + def _on_audio_changed(self, tab, _muted): + """Update audio field in tab when mute or recentlyAudible changed.""" + try: + idx = self._tab_index(tab) + except TabDeletedError: + # We can get signals for tabs we already deleted... + return + self.widget.update_tab_title(idx, 'audio') + if idx == self.widget.currentIndex(): + self._update_window_title('audio') + def _on_renderer_process_terminated(self, tab, status, code): """Show an error when a renderer process terminated.""" if status == browsertab.TerminationStatus.normal: diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 6a6eac901..8540ef24a 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -33,6 +33,7 @@ from PyQt5.QtGui import QIcon, QPalette, QColor from qutebrowser.utils import qtutils, objreg, utils, usertypes, log from qutebrowser.config import config from qutebrowser.misc import objects +from qutebrowser.browser import browsertab PixelMetrics = enum.IntEnum('PixelMetrics', ['icon_padding'], @@ -172,6 +173,16 @@ class TabWidget(QTabWidget): fields['perc_raw'] = tab.progress() fields['backend'] = objects.backend.name fields['private'] = ' [Private Mode] ' if tab.private else '' + try: + if tab.is_muted(): + fields['audio'] = '[M] ' + elif tab.is_recently_audible(): + fields['audio'] = '[A] ' + else: + fields['audio'] = '' + except browsertab.WebTabError: + # Muting is only implemented with QtWebEngine + fields['audio'] = '' if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress())