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