QtWebEngine: Implement mouse opentarget handling

This moves various stuff around and out of QtWebKit code:

- open_target and hint_target are now in TabData, not on the WebPage

- As much as possible got extracted from WebPage.acceptNavigationRequest
  to AbstractTab._on_link_clicked, with a new link_clicked signal added
  to WebPage. However, we need to decide whether to handle the request
  in this tab or not inside acceptNavigationRequest, so we have some
  code duplicated there and in WebEnginePage.acceptNavigationRequest.

- _mousepress_opentarget (i.e., setting the open_target) is now handled
  in MouseEventFilter, not in WebView.
This commit is contained in:
Florian Bruhin 2016-08-11 16:09:59 +02:00
parent 8b0028cb87
commit 3bffb71b55
7 changed files with 160 additions and 84 deletions

View File

@ -27,7 +27,8 @@ from PyQt5.QtWidgets import QWidget
from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, usertypes, message, log, qtutils
from qutebrowser.utils import (utils, objreg, usertypes, message, log, qtutils,
debug, urlutils)
from qutebrowser.misc import miscwidgets
from qutebrowser.browser import mouse
@ -68,14 +69,25 @@ class TabData:
load.
inspector: The QWebInspector used for this webview.
viewing_source: Set if we're currently showing a source view.
open_target: How the next clicked link should be opened.
hint_target: Override for open_target for hints.
"""
__slots__ = ['keep_icon', 'viewing_source', 'inspector']
__slots__ = ['keep_icon', 'viewing_source', 'inspector', 'open_target',
'hint_target']
def __init__(self):
self.keep_icon = False
self.viewing_source = False
self.inspector = None
self.open_target = usertypes.ClickTarget.normal
self.hint_target = None
def combined_target(self):
if self.hint_target is not None:
return self.hint_target
else:
return self.open_target
class AbstractPrinting:
@ -495,6 +507,43 @@ class AbstractTab(QWidget):
self._load_status = val
self.load_status_changed.emit(val.name)
@pyqtSlot(QUrl)
def _on_link_clicked(self, url):
log.webview.debug("link clicked: url {}, hint target {}, "
"open_target {}".format(
url.toDisplayString(),
self.data.hint_target, self.data.open_target))
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(self.win_id, msg)
self.data.open_target = usertypes.ClickTarget.normal
return False
target = self.data.combined_target()
if target == usertypes.ClickTarget.normal:
return
elif target == usertypes.ClickTarget.tab:
win_id = self.win_id
bg_tab = False
elif target == usertypes.ClickTarget.tab_bg:
win_id = self.win_id
bg_tab = True
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
bg_tab = False
else:
raise ValueError("Invalid ClickTarget {}".format(target))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(url, background=bg_tab)
self.data.open_target = usertypes.ClickTarget.normal
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
"""Update title when URL has changed and no title is available."""

View File

@ -21,7 +21,7 @@
from qutebrowser.config import config
from qutebrowser.utils import message, log
from qutebrowser.utils import message, log, usertypes
from PyQt5.QtCore import QObject, QEvent, Qt
@ -85,7 +85,10 @@ class MouseEventFilter(QObject):
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
self._mousepress_backforward(e)
return True
self._ignore_wheel_event = True
self._mousepress_opentarget(e)
return False
def _handle_wheel(self, _obj, e):
@ -131,6 +134,27 @@ class MouseEventFilter(QObject):
message.error(self._tab.win_id, "At end of history.",
immediately=True)
def _mousepress_opentarget(self, e):
"""Set the open target when something was clicked.
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs')
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
self._tab.data.open_target = target
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
target))
else:
self._tab.data.open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
def eventFilter(self, obj, event):
"""Filter events going to a QWeb(Engine)View."""
evtype = event.type()

View File

@ -322,7 +322,7 @@ class WebEngineTab(browsertab.AbstractTab):
def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id)
widget = webview.WebEngineView()
widget = webview.WebEngineView(tabdata=self.data)
self.history = WebEngineHistory(self)
self.scroller = WebEngineScroller(self, parent=self)
self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager,
@ -499,6 +499,7 @@ class WebEngineTab(browsertab.AbstractTab):
view.urlChanged.connect(self._on_url_changed)
page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors)
page.link_clicked.connect(self._on_link_clicked)
try:
view.iconChanged.connect(self.icon_changed)
except AttributeError:

View File

@ -20,29 +20,39 @@
"""The main browser widget for QtWebEngine."""
from PyQt5.QtCore import pyqtSignal, Qt, QPoint
from PyQt5.QtCore import pyqtSignal, Qt, QPoint, QUrl
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.config import config
from qutebrowser.utils import log
from qutebrowser.utils import log, debug, qtutils, usertypes
class WebEngineView(QWebEngineView):
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
def __init__(self, parent=None):
def __init__(self, tabdata, parent=None):
super().__init__(parent)
self.setPage(WebEnginePage(self))
self.setPage(WebEnginePage(tabdata, parent=self))
class WebEnginePage(QWebEnginePage):
"""Custom QWebEnginePage subclass with qutebrowser-specific features."""
"""Custom QWebEnginePage subclass with qutebrowser-specific features.
Signals:
certificate_error: FIXME:qtwebengine
link_clicked: Emitted when a link was clicked on a page.
"""
certificate_error = pyqtSignal()
link_clicked = pyqtSignal(QUrl)
def __init__(self, tabdata, parent=None):
super().__init__(parent)
self._tabdata = tabdata
def certificateError(self, error):
self.certificate_error.emit()
@ -68,3 +78,31 @@ class WebEnginePage(QWebEnginePage):
"""Handle new windows via JS."""
log.stub()
return None
def acceptNavigationRequest(self,
url: QUrl,
typ: QWebEnginePage.NavigationType,
_is_main_frame: bool):
"""Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound
to linkClicked won't work correctly, because when in a frameset, we
have no idea in which frame the link should be opened.
Checks if it should open it in a tab (middle-click or control) or not,
and then conditionally opens the URL. Opening it in a new tab/window
is handled in the slot connected to link_clicked.
"""
target = self._tabdata.combined_target()
log.webview.debug("navigation request: url {}, type {}, "
"target {}".format(
url.toDisplayString(),
debug.qenum_key(QWebEnginePage, typ),
target))
if typ != QWebEnginePage.NavigationTypeLinkClicked:
return True
self.link_clicked.emit(url)
return url.isValid() and target == usertypes.ClickTarget.normal

View File

@ -638,3 +638,4 @@ 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.link_clicked.connect(self._on_link_clicked)

View File

@ -26,14 +26,14 @@ from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from qutebrowser.config import config
from qutebrowser.browser import pdfjs
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
objreg, debug, urlutils)
objreg, debug)
class BrowserPage(QWebPage):
@ -42,27 +42,28 @@ class BrowserPage(QWebPage):
Attributes:
error_occurred: Whether an error occurred while loading.
open_target: Where to open the next navigation request.
("normal", "tab", "tab_bg")
_hint_target: Override for open_target while hinting, or None.
_extension_handlers: Mapping of QWebPage extensions to their handlers.
_networkmanager: The NetworkManager used.
_win_id: The window ID this BrowserPage is associated with.
_ignore_load_started: Whether to ignore the next loadStarted signal.
_is_shutting_down: Whether the page is currently shutting down.
_tabdata: The TabData object of the tab this page is in.
Signals:
shutting_down: Emitted when the page is currently shutting down.
reloading: Emitted before a web page reloads.
arg: The URL which gets reloaded.
link_clicked: Emitted when a link was clicked on a page.
"""
shutting_down = pyqtSignal()
reloading = pyqtSignal(QUrl)
link_clicked = pyqtSignal(QUrl)
def __init__(self, win_id, tab_id, parent=None):
def __init__(self, win_id, tab_id, tabdata, parent=None):
super().__init__(parent)
self._win_id = win_id
self._tabdata = tabdata
self._is_shutting_down = False
self._extension_handlers = {
QWebPage.ErrorPageExtension: self._handle_errorpage,
@ -70,8 +71,6 @@ class BrowserPage(QWebPage):
}
self._ignore_load_started = False
self.error_occurred = False
self.open_target = usertypes.ClickTarget.normal
self._hint_target = None
self._networkmanager = networkmanager.NetworkManager(
win_id, tab_id, self)
self.setNetworkAccessManager(self._networkmanager)
@ -429,14 +428,16 @@ class BrowserPage(QWebPage):
Args:
hint_target: A ClickTarget member to set self._hint_target to.
"""
# FIXME move this away
log.webview.debug("Setting force target to {}".format(hint_target))
self._hint_target = hint_target
self._tabdata.hint_target = hint_target
@pyqtSlot()
def on_stop_hinting(self):
"""Emitted when hinting is finished."""
# FIXME move this away
log.webview.debug("Finishing hinting.")
self._hint_target = None
self._tabdata.hint_target = None
def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent."""
@ -531,7 +532,10 @@ class BrowserPage(QWebPage):
answer = True
return answer
def acceptNavigationRequest(self, _frame, request, typ):
def acceptNavigationRequest(self,
_frame: QWebFrame,
request: QNetworkRequest,
typ: QWebPage.NavigationType):
"""Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound
@ -539,48 +543,23 @@ class BrowserPage(QWebPage):
have no idea in which frame the link should be opened.
Checks if it should open it in a tab (middle-click or control) or not,
and then opens the URL.
Args:
_frame: QWebFrame (target frame)
request: QNetworkRequest
typ: QWebPage::NavigationType
and then conditionally opens the URL. Opening it in a new tab/window
is handled in the slot connected to link_clicked.
"""
url = request.url()
urlstr = url.toDisplayString()
target = self._tabdata.combined_target()
log.webview.debug("navigation request: url {}, type {}, "
"target {}".format(
url.toDisplayString(),
debug.qenum_key(QWebPage, typ),
target))
if typ == QWebPage.NavigationTypeReload:
self.reloading.emit(url)
if typ != QWebPage.NavigationTypeLinkClicked:
return True
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(self._win_id, msg)
self.open_target = usertypes.ClickTarget.normal
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
"target {}, open_target {}".format(
urlstr, debug.qenum_key(QWebPage, typ),
self._hint_target, self.open_target))
if self._hint_target is not None:
target = self._hint_target
else:
target = self.open_target
self.open_target = usertypes.ClickTarget.normal
if target == usertypes.ClickTarget.tab:
tabbed_browser.tabopen(url, False)
return False
elif target == usertypes.ClickTarget.tab_bg:
tabbed_browser.tabopen(url, True)
return False
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id)
tabbed_browser.tabopen(url, False)
return False
else:
elif typ != QWebPage.NavigationTypeLinkClicked:
return True
self.link_clicked.emit(url)
return url.isValid() and target == usertypes.ClickTarget.normal

View File

@ -74,7 +74,7 @@ class WebView(QWebView):
self._set_bg_color()
self._tab_id = tab_id
page = self._init_page()
page = self._init_page(tab.data)
hintmanager = hints.HintManager(win_id, self._tab_id, self)
hintmanager.mouse_event.connect(self.on_mouse_event)
hintmanager.start_hinting.connect(page.on_start_hinting)
@ -89,9 +89,14 @@ class WebView(QWebView):
self.setContextMenuPolicy(Qt.PreventContextMenu)
objreg.get('config').changed.connect(self.on_config_changed)
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
def _init_page(self, tabdata):
"""Initialize the QWebPage used by this view.
Args:
tabdata: The TabData object for this tab.
"""
page = webpage.BrowserPage(self.win_id, self._tab_id, tabdata,
parent=self)
self.setPage(page)
page.mainFrame().loadFinished.connect(self.on_load_finished)
return page
@ -201,26 +206,6 @@ class WebView(QWebView):
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
'click-delayed')
def _mousepress_opentarget(self, e):
"""Set the open target when something was clicked.
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs')
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
self.page().open_target = target
log.mouse.debug("Middle click, setting target: {}".format(target))
else:
self.page().open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
def shutdown(self):
"""Shut down the webview."""
self.shutting_down.emit()
@ -382,7 +367,6 @@ class WebView(QWebView):
The superclass return value.
"""
self._mousepress_insertmode(e)
self._mousepress_opentarget(e)
super().mousePressEvent(e)
def mouseReleaseEvent(self, e):