diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 6ed5afe52..5598a0904 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QApplication from qutebrowser.keyinput import modeman from qutebrowser.config import config -from qutebrowser.utils import utils, objreg, usertypes, log, qtutils +from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, urlutils, + message) from qutebrowser.misc import miscwidgets, objects from qutebrowser.browser import mouse, hints @@ -94,6 +95,8 @@ class TabData: keep_icon: Whether the (e.g. cloned) icon should not be cleared on page load. inspector: The QWebInspector used for this webview. + open_target: Where to open the next link. + Only used for QtWebKit. override_target: Override for open_target for fake clicks (like hints). Only used for QtWebKit. pinned: Flag to pin the tab. @@ -104,6 +107,7 @@ class TabData: keep_icon = attr.ib(False) inspector = attr.ib(None) + open_target = attr.ib(usertypes.ClickTarget.normal) override_target = attr.ib(None) pinned = attr.ib(False) fullscreen = attr.ib(False) @@ -719,6 +723,22 @@ class AbstractTab(QWidget): self._set_load_status(usertypes.LoadStatus.loading) self.load_started.emit() + @pyqtSlot(usertypes.NavigationRequest) + def _on_navigation_request(self, navigation): + """Handle common acceptNavigationRequest code.""" + log.webview.debug("navigation request: url {}, type {}, is_main_frame " + "{}".format(navigation.url.toDisplayString(), + navigation.navigation_type, + navigation.is_main_frame)) + + if (navigation.navigation_type == navigation.Type.link_clicked and + not navigation.url.isValid()): + msg = urlutils.get_errstring(navigation.url, + "Invalid link clicked") + message.error(msg) + self.data.open_target = usertypes.ClickTarget.normal + navigation.accepted = False + def handle_auto_insert_mode(self, ok): """Handle `input.insert_mode.auto_load` after loading finished.""" if not config.val.input.insert_mode.auto_load or not ok: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 4e91ebd72..be559de54 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -886,6 +886,7 @@ class WebEngineTab(browsertab.AbstractTab): self._on_proxy_authentication_required) page.fullScreenRequested.connect(self._on_fullscreen_requested) page.contentsSizeChanged.connect(self.contents_size_changed) + page.navigation_request.connect(self._on_navigation_request) view.titleChanged.connect(self.title_changed) view.urlChanged.connect(self._on_url_changed) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 1b3c15f9e..ef666c934 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -124,10 +124,12 @@ class WebEnginePage(QWebEnginePage): Signals: certificate_error: Emitted on certificate errors. shutting_down: Emitted when the page is shutting down. + navigation_request: Emitted on acceptNavigationRequest. """ certificate_error = pyqtSignal() shutting_down = pyqtSignal() + navigation_request = pyqtSignal(usertypes.NavigationRequest) def __init__(self, *, theme_color, profile, parent=None): super().__init__(profile, parent) @@ -288,21 +290,26 @@ class WebEnginePage(QWebEnginePage): url: QUrl, typ: QWebEnginePage.NavigationType, is_main_frame: bool): - """Override acceptNavigationRequest to handle clicked links. - - This only show an error on invalid links - everything else is handled - in createWindow. - """ - log.webview.debug("navigation request: url {}, type {}, is_main_frame " - "{}".format(url.toDisplayString(), - debug.qenum_key(QWebEnginePage, typ), - is_main_frame)) - if (typ == QWebEnginePage.NavigationTypeLinkClicked and - not url.isValid()): - msg = urlutils.get_errstring(url, "Invalid link clicked") - message.error(msg) - return False - return True + """Override acceptNavigationRequest to forward it to the tab API.""" + type_map = { + QWebEnginePage.NavigationTypeLinkClicked: + usertypes.NavigationRequest.Type.link_clicked, + QWebEnginePage.NavigationTypeTyped: + usertypes.NavigationRequest.Type.typed, + QWebEnginePage.NavigationTypeFormSubmitted: + usertypes.NavigationRequest.Type.form_submitted, + QWebEnginePage.NavigationTypeBackForward: + usertypes.NavigationRequest.Type.back_forward, + QWebEnginePage.NavigationTypeReload: + usertypes.NavigationRequest.Type.reloaded, + QWebEnginePage.NavigationTypeOther: + usertypes.NavigationRequest.Type.other, + } + navigation = usertypes.NavigationRequest(url=url, + navigation_type=type_map[typ], + is_main_frame=is_main_frame) + self.navigation_request.emit(navigation) + return navigation.accepted @pyqtSlot('QUrl') def _inject_userjs(self, url): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 41e2432b3..165d2fdc4 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -35,7 +35,7 @@ from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKit import QWebSettings from PyQt5.QtPrintSupport import QPrinter -from qutebrowser.browser import browsertab +from qutebrowser.browser import browsertab, shared from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem, webkitsettings) from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug @@ -762,6 +762,25 @@ class WebKitTab(browsertab.AbstractTab): def _on_contents_size_changed(self, size): self.contents_size_changed.emit(QSizeF(size)) + @pyqtSlot(usertypes.NavigationRequest) + def _on_navigation_request(self, navigation): + super()._on_navigation_request(navigation) + log.webview.debug("target {} override {}".format( + self.data.open_target, self.data.override_target)) + + if self.data.override_target is not None: + target = self.data.override_target + self.data.override_target = None + else: + target = self.data.open_target + + if (navigation.navigation_type == navigation.Type.link_clicked and + target != usertypes.ClickTarget.normal): + tab = shared.get_tab(self.win_id, target) + tab.openurl(navigation.url) + self.data.open_target = usertypes.ClickTarget.normal + navigation.accepted = False + def _connect_signals(self): view = self._widget page = view.page() @@ -780,6 +799,7 @@ class WebKitTab(browsertab.AbstractTab): page.frameCreated.connect(self._on_frame_created) frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.initialLayoutCompleted.connect(self._on_history_trigger) + page.navigation_request.connect(self._on_navigation_request) self.url_changed.connect( functools.partial(webkitsettings.update_for_tab, self)) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 679ec2d88..89b205fa8 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -54,10 +54,12 @@ class BrowserPage(QWebPage): shutting_down: Emitted when the page is currently shutting down. reloading: Emitted before a web page reloads. arg: The URL which gets reloaded. + navigation_request: Emitted on acceptNavigationRequest. """ shutting_down = pyqtSignal() reloading = pyqtSignal(QUrl) + navigation_request = pyqtSignal(usertypes.NavigationRequest) def __init__(self, win_id, tab_id, tabdata, private, parent=None): super().__init__(parent) @@ -70,7 +72,6 @@ class BrowserPage(QWebPage): } self._ignore_load_started = False self.error_occurred = False - self.open_target = usertypes.ClickTarget.normal self._networkmanager = networkmanager.NetworkManager( win_id=win_id, tab_id=tab_id, private=private, parent=self) self.setNetworkAccessManager(self._networkmanager) @@ -474,7 +475,7 @@ class BrowserPage(QWebPage): source, line, msg) def acceptNavigationRequest(self, - _frame: QWebFrame, + frame: QWebFrame, request: QNetworkRequest, typ: QWebPage.NavigationType): """Override acceptNavigationRequest to handle clicked links. @@ -486,36 +487,27 @@ class BrowserPage(QWebPage): Checks if it should open it in a tab (middle-click or control) or not, and then conditionally opens the URL here or in another tab/window. """ - url = request.url() - log.webview.debug("navigation request: url {}, type {}, " - "target {} override {}".format( - url.toDisplayString(), - debug.qenum_key(QWebPage, typ), - self.open_target, - self._tabdata.override_target)) + type_map = { + QWebPage.NavigationTypeLinkClicked: + usertypes.NavigationRequest.Type.link_clicked, + QWebPage.NavigationTypeFormSubmitted: + usertypes.NavigationRequest.Type.form_submitted, + QWebPage.NavigationTypeFormResubmitted: + usertypes.NavigationRequest.Type.form_resubmitted, + QWebPage.NavigationTypeBackOrForward: + usertypes.NavigationRequest.Type.back_forward, + QWebPage.NavigationTypeReload: + usertypes.NavigationRequest.Type.reloaded, + QWebPage.NavigationTypeOther: + usertypes.NavigationRequest.Type.other, + } + is_main_frame = frame is self.mainFrame() + navigation = usertypes.NavigationRequest(url=request.url(), + navigation_type=type_map[typ], + is_main_frame=is_main_frame) - if self._tabdata.override_target is not None: - target = self._tabdata.override_target - self._tabdata.override_target = None - else: - target = self.open_target + if navigation.navigation_type == navigation.Type.reloaded: + self.reloading.emit(navigation.url) - if typ == QWebPage.NavigationTypeReload: - self.reloading.emit(url) - return True - elif typ != QWebPage.NavigationTypeLinkClicked: - return True - - if not url.isValid(): - msg = urlutils.get_errstring(url, "Invalid link clicked") - message.error(msg) - self.open_target = usertypes.ClickTarget.normal - return False - - if target == usertypes.ClickTarget.normal: - return True - - tab = shared.get_tab(self._win_id, target) - tab.openurl(url) - self.open_target = usertypes.ClickTarget.normal - return False + self.navigation_request.emit(navigation) + return navigation.accepted diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 942e7265c..79da9778c 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -262,10 +262,10 @@ class WebView(QWebView): target = usertypes.ClickTarget.tab_bg else: target = usertypes.ClickTarget.tab - self.page().open_target = target + self._tabdata.open_target = target log.mouse.debug("Ctrl/Middle click, setting target: {}".format( target)) else: - self.page().open_target = usertypes.ClickTarget.normal + self._tabdata.open_target = usertypes.ClickTarget.normal log.mouse.debug("Normal click, setting normal target") super().mousePressEvent(e) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 8312cd803..06b70fc21 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -27,6 +27,7 @@ import operator import collections.abc import enum +import attr from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from qutebrowser.utils import log, qtutils, utils @@ -394,3 +395,22 @@ class AbstractCertificateErrorWrapper: def is_overridable(self): raise NotImplementedError + + +@attr.s +class NavigationRequest: + + Type = enum.Enum('Type', [ + 'link_clicked', + 'typed', # QtWebEngine only + 'form_submitted', + 'form_resubmitted', # QtWebKit only + 'back_forward', + 'reloaded', + 'other' + ]) + + url = attr.ib() + navigation_type = attr.ib() + is_main_frame = attr.ib() + accepted = attr.ib(default=True) diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index dc14750e1..eb6a24df9 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -207,7 +207,7 @@ Feature: Using hints Scenario: Using :follow-hint inside an iframe When I open data/hints/iframe.html And I hint with args "links normal" and follow a - Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged + Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, *" should be logged Scenario: Using :follow-hint inside an iframe button When I open data/hints/iframe_button.html @@ -228,12 +228,12 @@ Feature: Using hints And I hint with args "all normal" and follow a And I run :scroll bottom And I hint with args "links normal" and follow a - Then "navigation request: url http://localhost:*/data/hello2.txt, type NavigationTypeLinkClicked, *" should be logged + Then "navigation request: url http://localhost:*/data/hello2.txt, type Type.link_clicked, *" should be logged Scenario: Opening a link inside a specific iframe When I open data/hints/iframe_target.html And I hint with args "links normal" and follow a - Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged + Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, *" should be logged Scenario: Opening a link with specific target frame in a new tab When I open data/hints/iframe_target.html diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index ae3f07999..4b379b8fe 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -250,7 +250,7 @@ Feature: Searching on a page And I run :search follow And I wait for "search found follow" in the log And I run :follow-selected - Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, is_main_frame False" should be logged + Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, is_main_frame False" should be logged @qtwebkit_skip: Not supported in qtwebkit Scenario: Follow a tabbed searched link in an iframe