diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 644af7ad5..98b0b7220 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -27,6 +27,7 @@ Added - Proxy support for QtWebEngine with Qt >= 5.8 - Support for the `content -> cookies-store` option with QtWebEngine - Support for the `storage -> cache-size` option with QtWebEngine +- Support for the HTML5 fullscreen API (e.g. youtube videos) with QtWebEngine Changed ~~~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 0e993b723..d6db9f350 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -319,8 +319,13 @@ How many pages to go forward. [[fullscreen]] === fullscreen +Syntax: +:fullscreen [*--leave*]+ + Toggle fullscreen mode. +==== optional arguments +* +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page. + [[help]] === help Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+ diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index b60267829..0e6d4b009 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -94,6 +94,18 @@ class TabData: self.override_target = None +class AbstractAction: + + """Attribute of AbstractTab for Qt WebActions.""" + + def __init__(self): + self._widget = None + + def exit_fullscreen(self): + """Exit the fullscreen mode.""" + raise NotImplementedError + + class AbstractPrinting: """Attribute of AbstractTab for printing the page.""" @@ -513,6 +525,9 @@ class AbstractTab(QWidget): new_tab_requested: Emitted when a new tab should be opened with the given URL. load_status_changed: The loading status changed + fullscreen_requested: Fullscreen display was requested by the page. + arg: True if fullscreen should be turned on, + False if it should be turned off. """ window_close_requested = pyqtSignal() @@ -528,6 +543,7 @@ class AbstractTab(QWidget): shutting_down = pyqtSignal() contents_size_changed = pyqtSignal(QSizeF) add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title + fullscreen_requested = pyqtSignal(bool) def __init__(self, win_id, mode_manager, parent=None): self.win_id = win_id @@ -548,6 +564,7 @@ class AbstractTab(QWidget): # self.search = AbstractSearch(parent=self) # self.printing = AbstractPrinting() # self.elements = AbstractElements(self) + # self.action = AbstractAction() self.data = TabData() self._layout = miscwidgets.WrapperLayout(self) @@ -576,6 +593,7 @@ class AbstractTab(QWidget): self.zoom._widget = widget self.search._widget = widget self.printing._widget = widget + self.action._widget = widget self.elements._widget = widget self._install_event_filter() diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index b01313cc0..fc131151f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -2069,3 +2069,24 @@ class CommandDispatcher: """ if bg or tab or window or url != old_url: self.openurl(url=url, bg=bg, tab=tab, window=window) + + @cmdutils.register(instance='command-dispatcher', scope='window') + def fullscreen(self, leave=False): + """Toggle fullscreen mode. + + Args: + leave: Only leave fullscreen if it was entered by the page. + """ + if leave: + tab = self._current_widget() + try: + tab.action.exit_fullscreen() + except browsertab.UnsupportedOperationError: + pass + return + + window = self._tabbed_browser.window() + if window.isFullScreen(): + window.showNormal() + else: + window.showFullScreen() diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 9db3e9d05..2be642b17 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -164,6 +164,8 @@ def init(args): # https://bugreports.qt.io/browse/QTBUG-58650 PersistentCookiePolicy().set(config.get('content', 'cookies-store')) + Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) + websettings.init_mappings(MAPPINGS) objreg.get('config').changed.connect(update_settings) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 4d5e405f4..83420dbfa 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -79,6 +79,17 @@ _JS_WORLD_MAP = { } +class WebEngineAction(browsertab.AbstractAction): + + """QtWebKit implementations related to web actions.""" + + def _action(self, action): + self._widget.triggerPageAction(action) + + def exit_fullscreen(self): + self._action(QWebEnginePage.ExitFullScreen) + + class WebEnginePrinting(browsertab.AbstractPrinting): """QtWebEngine implementations related to printing.""" @@ -473,6 +484,7 @@ class WebEngineTab(browsertab.AbstractTab): self.search = WebEngineSearch(parent=self) self.printing = WebEnginePrinting() self.elements = WebEngineElements(self) + self.action = WebEngineAction() self._set_widget(widget) self._connect_signals() self.backend = usertypes.Backend.QtWebEngine @@ -640,6 +652,12 @@ class WebEngineTab(browsertab.AbstractTab): url=url_string, error="Authentication required", icon='') self.set_html(error_page) + @pyqtSlot('QWebEngineFullScreenRequest') + def _on_fullscreen_requested(self, request): + # FIXME:qtwebengine do we want a setting to disallow this? + request.accept() + self.fullscreen_requested.emit(request.toggleOn()) + def _connect_signals(self): view = self._widget page = view.page() @@ -653,6 +671,7 @@ class WebEngineTab(browsertab.AbstractTab): page.loadFinished.connect(self._on_load_finished) page.certificate_error.connect(self._on_ssl_errors) page.authenticationRequired.connect(self._on_authentication_required) + page.fullScreenRequested.connect(self._on_fullscreen_requested) view.titleChanged.connect(self.title_changed) view.urlChanged.connect(self._on_url_changed) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index ff8534838..964ef60cc 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -52,6 +52,14 @@ def init(): objreg.register('js-bridge', js_bridge) +class WebKitAction(browsertab.AbstractAction): + + """QtWebKit implementations related to web actions.""" + + def exit_fullscreen(self): + raise browsertab.UnsupportedOperationError + + class WebKitPrinting(browsertab.AbstractPrinting): """QtWebKit implementations related to printing.""" @@ -610,6 +618,7 @@ class WebKitTab(browsertab.AbstractTab): self.search = WebKitSearch(parent=self) self.printing = WebKitPrinting() self.elements = WebKitElements(self) + self.action = WebKitAction() self._set_widget(widget) self._connect_signals() self.backend = usertypes.Backend.QtWebKit diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 6f3c08575..d08766af5 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1549,7 +1549,8 @@ KEY_DATA = collections.OrderedDict([ ])), ('normal', collections.OrderedDict([ - ('clear-keychain ;; search', ['', '']), + ('clear-keychain ;; search ;; fullscreen --leave', + ['', '']), ('set-cmd-text -s :open', ['o']), ('set-cmd-text :open {url:pretty}', ['go']), ('set-cmd-text -s :open -t', ['O']), @@ -1769,8 +1770,12 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^download-page$'), r'download'), (re.compile(r'^cancel-download$'), r'download-cancel'), - (re.compile(r"""^search (''|"")$"""), r'clear-keychain ;; search'), - (re.compile(r'^search$'), r'clear-keychain ;; search'), + (re.compile(r"""^search (''|"")$"""), + r'clear-keychain ;; search ;; fullscreen --leave'), + (re.compile(r'^search$'), + r'clear-keychain ;; search ;; fullscreen --leave'), + (re.compile(r'^clear-keychain ;; search$'), + r'clear-keychain ;; search ;; fullscreen --leave'), (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), @@ -1784,7 +1789,8 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^scroll 50 0$'), r'scroll right'), (re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'), - (re.compile(r'^search *;; *clear-keychain$'), r'clear-keychain ;; search'), + (re.compile(r'^search *;; *clear-keychain$'), + r'clear-keychain ;; search ;; fullscreen --leave'), (re.compile(r'^clear-keychain *;; *leave-mode$'), r'leave-mode'), (re.compile(r'^download-remove --all$'), r'download-clear'), diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 069c5c6b8..9606585e1 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -456,6 +456,10 @@ class MainWindow(QWidget): tabs.cur_url_changed.connect(status.url.set_url) tabs.cur_link_hovered.connect(status.url.set_hover_url) tabs.cur_load_status_changed.connect(status.url.on_load_status_changed) + tabs.page_fullscreen_requested.connect( + self._on_page_fullscreen_requested) + tabs.page_fullscreen_requested.connect( + status.on_page_fullscreen_requested) # command input / completion mode_manager.left.connect(tabs.on_mode_left) @@ -463,6 +467,13 @@ class MainWindow(QWidget): completion_obj.on_clear_completion_selection) cmd.hide_completion.connect(completion_obj.hide) + @pyqtSlot(bool) + def _on_page_fullscreen_requested(self, on): + if on: + self.showFullScreen() + else: + self.showNormal() + @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() def close(self): @@ -474,14 +485,6 @@ class MainWindow(QWidget): """ super().close() - @cmdutils.register(instance='main-window', scope='window') - def fullscreen(self): - """Toggle fullscreen mode.""" - if self.isFullScreen(): - self.showNormal() - else: - self.showFullScreen() - def resizeEvent(self, e): """Extend resizewindow's resizeEvent to adjust completion. diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 21e0d46ea..eaf8e6ffc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -46,6 +46,8 @@ class StatusBar(QWidget): _hbox: The main QHBoxLayout. _stack: The QStackedLayout with cmd/txt widgets. _win_id: The window ID the statusbar is associated with. + _page_fullscreen: Whether the webpage (e.g. a video) is shown + fullscreen. Class attributes: _prompt_active: If we're currently in prompt-mode. @@ -143,6 +145,7 @@ class StatusBar(QWidget): self._win_id = win_id self._option = None + self._page_fullscreen = False self._hbox = QHBoxLayout(self) self.set_hbox_padding() @@ -193,7 +196,7 @@ class StatusBar(QWidget): def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" hide = config.get('ui', 'hide-statusbar') - if hide: + if hide or self._page_fullscreen: self.hide() else: self.show() @@ -306,6 +309,11 @@ class StatusBar(QWidget): usertypes.KeyMode.yesno]: self.set_mode_active(old_mode, False) + @pyqtSlot(bool) + def on_page_fullscreen_requested(self, on): + self._page_fullscreen = on + self.maybe_hide() + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index d65360359..27a882dc7 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -98,6 +98,7 @@ class TabbedBrowser(tabwidget.TabWidget): resized = pyqtSignal('QRect') current_tab_changed = pyqtSignal(browsertab.AbstractTab) new_tab = pyqtSignal(browsertab.AbstractTab, int) + page_fullscreen_requested = pyqtSignal(bool) def __init__(self, win_id, parent=None): super().__init__(win_id, parent) @@ -199,6 +200,9 @@ class TabbedBrowser(tabwidget.TabWidget): functools.partial(self.on_window_close_requested, tab)) tab.new_tab_requested.connect(self.tabopen) tab.add_history_item.connect(objreg.get('web-history').add_from_tab) + tab.fullscreen_requested.connect(self.page_fullscreen_requested) + tab.fullscreen_requested.connect( + self.tabBar().on_page_fullscreen_requested) def current_url(self): """Get the URL of the current tab. diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index c639c293f..ce9a90f4d 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -259,6 +259,8 @@ class TabBar(QTabBar): Attributes: vertical: When the tab bar is currently vertical. win_id: The window ID this TabBar belongs to. + _page_fullscreen: Whether the webpage (e.g. a video) is shown + fullscreen. """ def __init__(self, win_id, parent=None): @@ -269,6 +271,7 @@ class TabBar(QTabBar): config_obj = objreg.get('config') config_obj.changed.connect(self.set_font) self.vertical = False + self._page_fullscreen = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) self._auto_hide_timer.setInterval( @@ -296,20 +299,24 @@ class TabBar(QTabBar): self._auto_hide_timer.setInterval( config.get('tabs', 'show-switching-delay')) + @pyqtSlot(bool) + def on_page_fullscreen_requested(self, on): + self._page_fullscreen = on + self._tabhide() + def on_change(self): """Show tab bar when current tab got changed.""" show = config.get('tabs', 'show') - if show == 'switching': + if show == 'switching' or self._page_fullscreen: self.show() self._auto_hide_timer.start() def _tabhide(self): """Hide the tab bar if needed.""" show = config.get('tabs', 'show') - show_never = show == 'never' - switching = show == 'switching' - multiple = show == 'multiple' - if show_never or (multiple and self.count() == 1) or switching: + if (show in ['never', 'switching'] or + (show == 'multiple' and self.count() == 1) or + self._page_fullscreen): self.hide() else: self.show() diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index d5324e597..7ff1edbde 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -110,6 +110,7 @@ class Tab(browsertab.AbstractTab): self.search = browsertab.AbstractSearch(parent=self) self.printing = browsertab.AbstractPrinting() self.elements = browsertab.AbstractElements(self) + self.action = browsertab.AbstractAction() def _install_event_filter(self): pass diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 6931866ed..0c3eda341 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -281,9 +281,9 @@ class TestKeyConfigParser: ('download-page', 'download'), ('cancel-download', 'download-cancel'), - ('search ""', 'clear-keychain ;; search'), - ("search ''", 'clear-keychain ;; search'), - ("search", 'clear-keychain ;; search'), + ('search ""', 'clear-keychain ;; search ;; fullscreen --leave'), + ("search ''", 'clear-keychain ;; search ;; fullscreen --leave'), + ("search", 'clear-keychain ;; search ;; fullscreen --leave'), ("search ;; foobar", None), ('search "foo"', None), @@ -305,11 +305,16 @@ class TestKeyConfigParser: ('scroll 0 0', 'scroll-px 0 0'), ('scroll 23 42', 'scroll-px 23 42'), - ('search ;; clear-keychain', 'clear-keychain ;; search'), - ('search;;clear-keychain', 'clear-keychain ;; search'), + ('search ;; clear-keychain', + 'clear-keychain ;; search ;; fullscreen --leave'), + ('search;;clear-keychain', + 'clear-keychain ;; search ;; fullscreen --leave'), ('search;;foo', None), - ('clear-keychain ;; leave-mode', 'leave-mode'), + ('clear-keychain ;; search', + 'clear-keychain ;; search ;; fullscreen --leave'), ('leave-mode ;; foo', None), + ('search ;; clear-keychain', + 'clear-keychain ;; search ;; fullscreen --leave'), ('download-remove --all', 'download-clear'),