summaryrefslogtreecommitdiff
path: root/qutebrowser/misc/nativeeventfilter.py
blob: 06533bd42a34c44eb77d3fc133d9a0646902961f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Native Qt event filter.

This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334.
"""

from typing import Tuple, Union, cast, Optional
import enum
import ctypes
import ctypes.util

from qutebrowser.qt import sip, machinery
from qutebrowser.qt.core import QAbstractNativeEventFilter, QByteArray, qVersion

from qutebrowser.misc import objects
from qutebrowser.utils import log


# Needs to be saved to avoid garbage collection
_instance: Optional["NativeEventFilter"] = None

# Using C-style naming for C structures in this file
# pylint: disable=invalid-name


class xcb_ge_generic_event_t(ctypes.Structure):  # noqa: N801
    """See https://xcb.freedesktop.org/manual/structxcb__ge__generic__event__t.html.

    Also used for xcb_generic_event_t as the structures overlap:
    https://xcb.freedesktop.org/manual/structxcb__generic__event__t.html
    """

    _fields_ = [
        ("response_type", ctypes.c_uint8),
        ("extension", ctypes.c_uint8),
        ("sequence", ctypes.c_uint16),
        ("length", ctypes.c_uint32),
        ("event_type", ctypes.c_uint16),
        ("pad0", ctypes.c_uint8 * 22),
        ("full_sequence", ctypes.c_uint32),
    ]


_XCB_GE_GENERIC = 35


class XcbInputOpcodes(enum.IntEnum):

    """https://xcb.freedesktop.org/manual/group__XCB__Input__API.html.

    NOTE: If adding anything new here, adjust _PROBLEMATIC_XINPUT_EVENTS below!
    """

    HIERARCHY = 11

    TOUCH_BEGIN = 18
    TOUCH_UPDATE = 19
    TOUCH_END = 20

    GESTURE_PINCH_BEGIN = 27
    GESTURE_PINCH_UPDATE = 28
    GESTURE_PINCH_END = 29

    GESTURE_SWIPE_BEGIN = 30
    GESTURE_SWIPE_UPDATE = 31
    GESTURE_SWIPE_END = 32


_PROBLEMATIC_XINPUT_EVENTS = set(XcbInputOpcodes) - {XcbInputOpcodes.HIERARCHY}


class xcb_query_extension_reply_t(ctypes.Structure):  # noqa: N801
    """https://xcb.freedesktop.org/manual/structxcb__query__extension__reply__t.html."""

    _fields_ = [
        ("response_type", ctypes.c_uint8),
        ("pad0", ctypes.c_uint8),
        ("sequence", ctypes.c_uint16),
        ("length", ctypes.c_uint32),
        ("present", ctypes.c_uint8),
        ("major_opcode", ctypes.c_uint8),
        ("first_event", ctypes.c_uint8),
        ("first_error", ctypes.c_uint8),
    ]


# pylint: enable=invalid-name


if machinery.IS_QT6:
    _PointerRetType = sip.voidptr
else:
    _PointerRetType = int


class NativeEventFilter(QAbstractNativeEventFilter):

    """Event filter for XCB messages to work around Qt 6.5.1 crash."""

    # Return values for nativeEventFilter.
    #
    # Tuple because PyQt uses the second value as the *result out-pointer, which
    # according to the Qt documentation is only used on Windows.
    _PASS_EVENT_RET = (False, cast(_PointerRetType, 0))
    _FILTER_EVENT_RET = (True, cast(_PointerRetType, 0))

    def __init__(self) -> None:
        super().__init__()
        self._active = False  # Set to true when getting hierarchy event

        xcb = ctypes.CDLL(ctypes.util.find_library("xcb"))
        xcb.xcb_connect.restype = ctypes.POINTER(ctypes.c_void_p)
        xcb.xcb_query_extension_reply.restype = ctypes.POINTER(
            xcb_query_extension_reply_t
        )

        conn = xcb.xcb_connect(None, None)
        assert conn

        try:
            assert not xcb.xcb_connection_has_error(conn)

            # Get major opcode ID of Xinput extension
            name = b"XInputExtension"
            cookie = xcb.xcb_query_extension(conn, len(name), name)
            reply = xcb.xcb_query_extension_reply(conn, cookie, None)
            assert reply

            if reply.contents.present:
                self.xinput_opcode = reply.contents.major_opcode
            else:
                self.xinput_opcode = None
        finally:
            xcb.xcb_disconnect(conn)

    def nativeEventFilter(
        self, evtype: Union[bytes, QByteArray], message: Optional[sip.voidptr]
    ) -> Tuple[bool, _PointerRetType]:
        """Handle XCB events."""
        # We're only installed when the platform plugin is xcb
        assert evtype == b"xcb_generic_event_t", evtype
        assert message is not None

        # We cast to xcb_ge_generic_event_t, which overlaps with xcb_generic_event_t.
        # .extension and .event_type will only make sense if this is an
        # XCB_GE_GENERIC event, but this is the first thing we check in the 'if'
        # below anyways.
        event = ctypes.cast(
            int(message), ctypes.POINTER(xcb_ge_generic_event_t)
        ).contents

        if (
            event.response_type == _XCB_GE_GENERIC
            and event.extension == self.xinput_opcode
        ):
            if not self._active and event.event_type == XcbInputOpcodes.HIERARCHY:
                log.misc.warning(
                    "Got XInput HIERARCHY event, future swipe/pinch/touch events will "
                    "be ignored to avoid a Qt 6.5.1 crash. Restart qutebrowser to make "
                    "them work again."
                )
                self._active = True
            elif self._active and event.event_type in _PROBLEMATIC_XINPUT_EVENTS:
                name = XcbInputOpcodes(event.event_type).name
                log.misc.debug(f"Ignoring problematic XInput event {name}")
                return self._FILTER_EVENT_RET

        return self._PASS_EVENT_RET


def init() -> None:
    """Install the native event filter if needed."""
    global _instance

    platform = objects.qapp.platformName()
    qt_version = qVersion()
    log.misc.debug(f"Platform {platform}, Qt {qt_version}")

    if platform != "xcb" or qt_version != "6.5.1":
        return

    log.misc.debug("Installing native event filter to work around Qt 6.5.1 crash")
    _instance = NativeEventFilter()
    objects.qapp.installNativeEventFilter(_instance)