Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions v3/pkg/application/systemtray_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ const (
// system tray map
var systemTrayMap = make(map[uint]*macosSystemTray)

// coerceStatusItemEventType wraps the C helper; used by tests.
// See systemtray_darwin.h for the #5752 rationale.
func coerceStatusItemEventType(rawEventType int, pressedMouseButtons uint64) int {
return int(C.systemTrayCoerceEventType(C.int(rawEventType), C.ulong(pressedMouseButtons)))
}

//export systrayClickCallback
func systrayClickCallback(id C.long, buttonID C.int) {
// Get the system tray
Expand Down
10 changes: 9 additions & 1 deletion v3/pkg/application/systemtray_darwin.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen);
NSRect NSScreen_frame(void* screen);
void windowSetScreen(void* window, void* screen, int yOffset);
int statusBarHeight();
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset);
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset);

// systemTrayCoerceEventType maps the raw NSEvent.type observed inside
// -[StatusItemController statusItemClicked:] to a mouse-button event type
// that Go's processClick can dispatch on. On current macOS betas the
// currentEvent at action time is no longer the originating mouse-down
// (typically a later NSEventTypeMouseMoved), so callers fall back to
// [NSEvent pressedMouseButtons]. Exposed for regression testing (#5752).
int systemTrayCoerceEventType(int rawEventType, unsigned long pressedMouseButtons);
18 changes: 17 additions & 1 deletion v3/pkg/application/systemtray_darwin.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,28 @@
extern void systrayClickCallback(long, int);
extern int systrayPreClickCallback(long, int);

// Bit returned by -[NSEvent pressedMouseButtons] for the right mouse button.
static const NSUInteger kRightMouseButtonBit = 1UL << 1;

// See systemtray_darwin.h for the #5752 rationale.
int systemTrayCoerceEventType(int rawEventType, unsigned long pressedMouseButtons) {
if (rawEventType == NSEventTypeLeftMouseDown || rawEventType == NSEventTypeRightMouseDown) {
return rawEventType;
}
if (pressedMouseButtons & kRightMouseButtonBit) {
return (int)NSEventTypeRightMouseDown;
}
return (int)NSEventTypeLeftMouseDown;
}

// StatusItemController.m
@implementation StatusItemController

- (void)statusItemClicked:(id)sender {
NSEvent *event = [NSApp currentEvent];
systrayClickCallback(self.id, event.type);
systrayClickCallback(self.id,
systemTrayCoerceEventType((int)event.type,
(unsigned long)[NSEvent pressedMouseButtons]));
}

- (void)menuDidClose:(NSMenu *)menu {
Expand Down
101 changes: 101 additions & 0 deletions v3/pkg/application/systemtray_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//go:build darwin && !ios && !server

package application

import "testing"

// NSEventType and pressedMouseButtons bit values from AppKit, duplicated
// here so the test does not need to import AppKit.
const (
testNSEventTypeLeftMouseDown = 1
testNSEventTypeRightMouseDown = 3
testNSEventTypeLeftMouseUp = 2
testNSEventTypeMouseMoved = 5
testNSEventTypeMouseEntered = 8
testNSEventTypeCursorUpdate = 17

testPressedNone = 0
testPressedLeft = 1 << 0
testPressedRight = 1 << 1
)

// TestCoerceStatusItemEventType_MacOS27Regression pins the fix for #5752.
// See systemtray_darwin.h for the rationale.
func TestCoerceStatusItemEventType_MacOS27Regression(t *testing.T) {
tests := []struct {
name string
rawEventType int
pressed uint64
wantEventType int
}{
// macOS <=26 path: currentEvent still is the mouse-down; helper is
// a no-op.
{
name: "left mouse-down passes through unchanged (<=26)",
rawEventType: testNSEventTypeLeftMouseDown,
pressed: testPressedLeft,
wantEventType: testNSEventTypeLeftMouseDown,
},
{
name: "right mouse-down passes through unchanged (<=26)",
rawEventType: testNSEventTypeRightMouseDown,
pressed: testPressedRight,
wantEventType: testNSEventTypeRightMouseDown,
},

// macOS 27 regression path: raw type is *not* a mouse-down, so the
// helper reads pressedMouseButtons to recover the intended button.
// The MouseMoved+pressed=1 case is the exact combination observed
// in field logs on macOS 27 for a plain left click on the tray icon.
{
name: "macOS 27: MouseMoved with left pressed -> LeftMouseDown",
rawEventType: testNSEventTypeMouseMoved,
pressed: testPressedLeft,
wantEventType: testNSEventTypeLeftMouseDown,
},
{
name: "macOS 27: MouseMoved with right pressed -> RightMouseDown",
rawEventType: testNSEventTypeMouseMoved,
pressed: testPressedRight,
wantEventType: testNSEventTypeRightMouseDown,
},
{
name: "macOS 27: CursorUpdate with left pressed -> LeftMouseDown",
rawEventType: testNSEventTypeCursorUpdate,
pressed: testPressedLeft,
wantEventType: testNSEventTypeLeftMouseDown,
},
{
name: "macOS 27: MouseEntered with no button pressed -> LeftMouseDown (default)",
rawEventType: testNSEventTypeMouseEntered,
pressed: testPressedNone,
wantEventType: testNSEventTypeLeftMouseDown,
},
{
name: "macOS 27: LeftMouseUp with left pressed -> LeftMouseDown",
rawEventType: testNSEventTypeLeftMouseUp,
pressed: testPressedLeft,
wantEventType: testNSEventTypeLeftMouseDown,
},

// Right takes precedence when both bits are set: this matches
// existing right-click semantics (right-click hides the attached
// window and opens the menu).
{
name: "both buttons pressed -> RightMouseDown wins",
rawEventType: testNSEventTypeMouseMoved,
pressed: testPressedLeft | testPressedRight,
wantEventType: testNSEventTypeRightMouseDown,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := coerceStatusItemEventType(tc.rawEventType, tc.pressed)
if got != tc.wantEventType {
t.Fatalf("coerceStatusItemEventType(raw=%d, pressed=%#x) = %d, want %d",
tc.rawEventType, tc.pressed, got, tc.wantEventType)
}
})
}
}
Loading