Stop Qt's tray icon from crashing the app on macOS 27. Returns whether it is installed.
Qt 6.11.2 (and 6.12.0) reads [NSApp currentEvent].clickCount when the status item menu
opens. On macOS 27 that event can be a MouseExited or a gesture event, and AppKit raises
NSInternalInconsistencyException ("Invalid message sent to event"), which ends the process
after a few clicks on the icon. Fixed in Qt (qtbase 65020b4, "Don't assume the current event
is a mouse event in the tray icon"), but not yet in a PySide6 release on PyPI.
This replaces clickCount, in this process only, by a version that answers 0 for an event
that is not a mouse button event and asks the original for the others. Qt then sees a plain
click, which is what the fix does. Remove it once naneos-devices requires a PySide6 with the
Qt fix.
Source code in src/naneos/gui/macos.py
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 | def guard_event_click_count() -> bool:
"""Stop Qt's tray icon from crashing the app on macOS 27. Returns whether it is installed.
Qt 6.11.2 (and 6.12.0) reads `[NSApp currentEvent].clickCount` when the status item menu
opens. On macOS 27 that event can be a MouseExited or a gesture event, and AppKit raises
NSInternalInconsistencyException ("Invalid message sent to event"), which ends the process
after a few clicks on the icon. Fixed in Qt (qtbase 65020b4, "Don't assume the current event
is a mouse event in the tray icon"), but not yet in a PySide6 release on PyPI.
This replaces clickCount, in this process only, by a version that answers 0 for an event
that is not a mouse button event and asks the original for the others. Qt then sees a plain
click, which is what the fix does. Remove it once naneos-devices requires a PySide6 with the
Qt fix.
"""
global _click_count_guard
if _click_count_guard is not None:
return True
try:
libobjc = ctypes.CDLL(ctypes.util.find_library("objc") or "/usr/lib/libobjc.A.dylib")
pointer = ctypes.c_void_p
libobjc.objc_getClass.restype = pointer
libobjc.objc_getClass.argtypes = [ctypes.c_char_p]
libobjc.sel_registerName.restype = pointer
libobjc.sel_registerName.argtypes = [ctypes.c_char_p]
libobjc.class_getInstanceMethod.restype = pointer
libobjc.class_getInstanceMethod.argtypes = [pointer, pointer]
libobjc.method_getImplementation.restype = pointer
libobjc.method_getImplementation.argtypes = [pointer]
libobjc.method_setImplementation.restype = pointer
libobjc.method_setImplementation.argtypes = [pointer, pointer]
method = libobjc.class_getInstanceMethod(
libobjc.objc_getClass(b"NSEvent"), libobjc.sel_registerName(b"clickCount")
)
if not method:
raise RuntimeError("NSEvent has no clickCount")
type_selector = libobjc.sel_registerName(b"type")
click_count_type = ctypes.CFUNCTYPE(ctypes.c_long, pointer, pointer) # NSInteger
event_type_type = ctypes.CFUNCTYPE(ctypes.c_ulong, pointer, pointer) # NSUInteger
original = click_count_type(libobjc.method_getImplementation(method))
event_type = event_type_type(("objc_msgSend", libobjc)) # [event type]
def click_count(event: int, selector: int) -> int:
if event_type(event, type_selector) in _MOUSE_EVENT_TYPES:
return int(original(event, selector))
return 0
callback = click_count_type(click_count)
libobjc.method_setImplementation(method, ctypes.cast(callback, pointer))
_click_count_guard = callback
return True
except Exception as e: # without it the tray can crash on macOS 27, but only there
logger.warning(f"Could not install the clickCount guard: {e}")
return False
|