1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2016-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 4# 5# This file is part of qutebrowser. 6# 7# qutebrowser is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# qutebrowser is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. 19 20"""Event handling for a browser tab.""" 21 22from PyQt5.QtCore import QObject, QEvent, Qt, QTimer 23 24from qutebrowser.config import config 25from qutebrowser.utils import message, log, usertypes, qtutils 26from qutebrowser.misc import objects 27from qutebrowser.keyinput import modeman 28 29 30class ChildEventFilter(QObject): 31 32 """An event filter re-adding TabEventFilter on ChildEvent. 33 34 This is needed because QtWebEngine likes to randomly change its 35 focusProxy... 36 37 FIXME:qtwebengine Add a test for this happening 38 39 Attributes: 40 _filter: The event filter to install. 41 _widget: The widget expected to send out childEvents. 42 """ 43 44 def __init__(self, *, eventfilter, widget=None, parent=None): 45 super().__init__(parent) 46 self._filter = eventfilter 47 self._widget = widget 48 49 def eventFilter(self, obj, event): 50 """Act on ChildAdded events.""" 51 if event.type() == QEvent.ChildAdded: 52 child = event.child() 53 log.misc.debug("{} got new child {}, installing filter" 54 .format(obj, child)) 55 56 # Additional sanity check, but optional 57 if self._widget is not None: 58 assert obj is self._widget 59 60 child.installEventFilter(self._filter) 61 elif event.type() == QEvent.ChildRemoved: 62 child = event.child() 63 log.misc.debug("{}: removed child {}".format(obj, child)) 64 65 return False 66 67 68class TabEventFilter(QObject): 69 70 """Handle mouse/keyboard events on a tab. 71 72 Attributes: 73 _tab: The browsertab object this filter is installed on. 74 _handlers: A dict of handler functions for the handled events. 75 _ignore_wheel_event: Whether to ignore the next wheelEvent. 76 _check_insertmode_on_release: Whether an insertmode check should be 77 done when the mouse is released. 78 """ 79 80 def __init__(self, tab, *, parent=None): 81 super().__init__(parent) 82 self._tab = tab 83 self._handlers = { 84 QEvent.MouseButtonPress: self._handle_mouse_press, 85 QEvent.MouseButtonRelease: self._handle_mouse_release, 86 QEvent.Wheel: self._handle_wheel, 87 QEvent.KeyRelease: self._handle_key_release, 88 } 89 self._ignore_wheel_event = False 90 self._check_insertmode_on_release = False 91 92 def _handle_mouse_press(self, e): 93 """Handle pressing of a mouse button. 94 95 Args: 96 e: The QMouseEvent. 97 98 Return: 99 True if the event should be filtered, False otherwise. 100 """ 101 is_rocker_gesture = (config.val.input.mouse.rocker_gestures and 102 e.buttons() == Qt.LeftButton | Qt.RightButton) 103 104 if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture: 105 self._mousepress_backforward(e) 106 return True 107 108 self._ignore_wheel_event = True 109 110 pos = e.pos() 111 if pos.x() < 0 or pos.y() < 0: 112 log.mouse.warning("Ignoring invalid click at {}".format(pos)) 113 return False 114 115 if e.button() != Qt.NoButton: 116 self._tab.elements.find_at_pos(pos, self._mousepress_insertmode_cb) 117 118 return False 119 120 def _handle_mouse_release(self, _e): 121 """Handle releasing of a mouse button. 122 123 Args: 124 e: The QMouseEvent. 125 126 Return: 127 True if the event should be filtered, False otherwise. 128 """ 129 # We want to make sure we check the focus element after the WebView is 130 # updated completely. 131 QTimer.singleShot(0, self._mouserelease_insertmode) 132 return False 133 134 def _handle_wheel(self, e): 135 """Zoom on Ctrl-Mousewheel. 136 137 Args: 138 e: The QWheelEvent. 139 140 Return: 141 True if the event should be filtered, False otherwise. 142 """ 143 if self._ignore_wheel_event: 144 # See https://github.com/qutebrowser/qutebrowser/issues/395 145 self._ignore_wheel_event = False 146 return True 147 148 # Don't allow scrolling while hinting 149 mode = modeman.instance(self._tab.win_id).mode 150 if mode == usertypes.KeyMode.hint: 151 return True 152 153 elif e.modifiers() & Qt.ControlModifier: 154 if mode == usertypes.KeyMode.passthrough: 155 return False 156 157 divider = config.val.zoom.mouse_divider 158 if divider == 0: 159 # Disable mouse zooming 160 return True 161 162 factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) 163 if factor < 0: 164 return True 165 166 perc = int(100 * factor) 167 message.info(f"Zoom level: {perc}%", replace='zoom-level') 168 self._tab.zoom.set_factor(factor) 169 return True 170 171 return False 172 173 def _handle_key_release(self, e): 174 """Ignore repeated key release events going to the website. 175 176 WORKAROUND for https://bugreports.qt.io/browse/QTBUG-77208 177 178 Args: 179 e: The QKeyEvent. 180 181 Return: 182 True if the event should be filtered, False otherwise. 183 """ 184 return (e.isAutoRepeat() and 185 not qtutils.version_check('5.14', compiled=False) and 186 objects.backend == usertypes.Backend.QtWebEngine) 187 188 def _mousepress_insertmode_cb(self, elem): 189 """Check if the clicked element is editable.""" 190 if elem is None: 191 # Something didn't work out, let's find the focus element after 192 # a mouse release. 193 log.mouse.debug("Got None element, scheduling check on " 194 "mouse release") 195 self._check_insertmode_on_release = True 196 return 197 198 if elem.is_editable(): 199 log.mouse.debug("Clicked editable element!") 200 if config.val.input.insert_mode.auto_enter: 201 modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, 202 'click', only_if_normal=True) 203 else: 204 log.mouse.debug("Clicked non-editable element!") 205 if config.val.input.insert_mode.auto_leave: 206 modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, 207 'click', maybe=True) 208 209 def _mouserelease_insertmode(self): 210 """If we have an insertmode check scheduled, handle it.""" 211 if not self._check_insertmode_on_release: 212 return 213 self._check_insertmode_on_release = False 214 215 def mouserelease_insertmode_cb(elem): 216 """Callback which gets called from JS.""" 217 if elem is None: 218 log.mouse.debug("Element vanished!") 219 return 220 221 if elem.is_editable(): 222 log.mouse.debug("Clicked editable element (delayed)!") 223 modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, 224 'click-delayed', only_if_normal=True) 225 else: 226 log.mouse.debug("Clicked non-editable element (delayed)!") 227 if config.val.input.insert_mode.auto_leave: 228 modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, 229 'click-delayed', maybe=True) 230 231 self._tab.elements.find_focused(mouserelease_insertmode_cb) 232 233 def _mousepress_backforward(self, e): 234 """Handle back/forward mouse button presses. 235 236 Args: 237 e: The QMouseEvent. 238 239 Return: 240 True if the event should be filtered, False otherwise. 241 """ 242 if (not config.val.input.mouse.back_forward_buttons and 243 e.button() in [Qt.XButton1, Qt.XButton2]): 244 # Back and forward on mice are disabled 245 return 246 247 if e.button() in [Qt.XButton1, Qt.LeftButton]: 248 # Back button on mice which have it, or rocker gesture 249 if self._tab.history.can_go_back(): 250 self._tab.history.back() 251 else: 252 message.error("At beginning of history.") 253 elif e.button() in [Qt.XButton2, Qt.RightButton]: 254 # Forward button on mice which have it, or rocker gesture 255 if self._tab.history.can_go_forward(): 256 self._tab.history.forward() 257 else: 258 message.error("At end of history.") 259 260 def eventFilter(self, obj, event): 261 """Filter events going to a QWeb(Engine)View. 262 263 Return: 264 True if the event should be filtered, False otherwise. 265 """ 266 evtype = event.type() 267 if evtype not in self._handlers: 268 return False 269 if obj is not self._tab.private_api.event_target(): 270 log.mouse.debug("Ignoring {} to {}".format( 271 event.__class__.__name__, obj)) 272 return False 273 return self._handlers[evtype](event) 274